@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,453 @@
|
|
|
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 { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './lookups.js';
|
|
26
|
+
import { prepareCommand } from './cli-spelling.js';
|
|
27
|
+
/** Flow's dialect, re-exported so `serialize.ts` need not reach past this module. */
|
|
28
|
+
export { FLOW_EXPR_DIALECT } from './connector-checks.js';
|
|
29
|
+
import { FLOW_EXPR_DIALECT as FLOW_DIALECT } from './connector-checks.js';
|
|
30
|
+
/**
|
|
31
|
+
* Validate a connector's activity inputs against its declared field list and
|
|
32
|
+
* return them ROUTED to the wire slots the platform reads. Catches the three
|
|
33
|
+
* connector-input mistakes — missing required, unknown key, type mismatch — at
|
|
34
|
+
* compile, where the author can act on them.
|
|
35
|
+
*
|
|
36
|
+
* Generated descriptors accept natural nested objects and arrays; the legacy
|
|
37
|
+
* string form also accepts exact flat registry field ids. Routing remains the
|
|
38
|
+
* library's business because a field's location is a property of the operation.
|
|
39
|
+
* Each projected registry field lands in the slot its declaration names:
|
|
40
|
+
*
|
|
41
|
+
* ```jsonc
|
|
42
|
+
* // connector('uipath-atlassian-jira','get-issue', { issueId: …, project: 'IN', issuetype: 'Task' })
|
|
43
|
+
* "inputs": { "detail": {
|
|
44
|
+
* "endpoint": "/curated_get_issue/{issueId}",
|
|
45
|
+
* "pathParameters": { "issueId": "IN-28" },
|
|
46
|
+
* "queryParameters": { "project": "IN", "issuetype": "Task" },
|
|
47
|
+
* "bodyParameters": {} } }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* That is the platform's own encoding, measured on the deployed corpus: 1,283
|
|
51
|
+
* connector nodes in the nightly artifacts, 384 carrying `pathParameters` and
|
|
52
|
+
* 576 carrying `queryParameters`, always as a FLAT map keyed by the parameter's
|
|
53
|
+
* DECLARED name (`send_as`, `issuetype`, `channel_id` keep their spelling — no
|
|
54
|
+
* camelCasing) and always as a SIBLING of `bodyParameters` rather than as a
|
|
55
|
+
* location-tagged entry inside it.
|
|
56
|
+
*/
|
|
57
|
+
/** For a missing required lookup field: what it sends and how to resolve it. Empty for a plain field. */
|
|
58
|
+
function missingLookupHint(field, lookup, spec, descriptors) {
|
|
59
|
+
if (!lookup)
|
|
60
|
+
return '';
|
|
61
|
+
const target = `${lookup.objectName ?? lookup.path}.${lookup.value}`;
|
|
62
|
+
const alias = Object.keys(lookup.aliases)[0];
|
|
63
|
+
const by = lookup.by[0] ?? 'id';
|
|
64
|
+
// `<Descriptor>` is a placeholder only a Flow author can fill, from a
|
|
65
|
+
// generated `connectors/<key>.ts`. BPMN imports none, so it gets the string
|
|
66
|
+
// form with the real key and action — which is the call that compiles there.
|
|
67
|
+
const call = descriptors
|
|
68
|
+
? (alias
|
|
69
|
+
? `lookup(<Descriptor>, '${field}').${alias}('<value>')`
|
|
70
|
+
: `lookup(<Descriptor>, '${field}').by('${by}', '<value>')`)
|
|
71
|
+
: `lookup('${spec.key ?? '<key>'}', '${spec.action ?? '<action>'}', '${field}').by('${by}', '<value>')`;
|
|
72
|
+
return ` — a lookup id (\`${target}\`), not a name or address: ${call}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* What `compile` would refuse these connector inputs with, or `undefined` when it
|
|
76
|
+
* would accept them — the SAME code path `buildConnectorInputs` runs at emit time,
|
|
77
|
+
* so `check` and `compile` cannot disagree about a missing required field, an
|
|
78
|
+
* unknown or mistyped input, or a container part passed as a value. Unresolved
|
|
79
|
+
* lookups are the one thing skipped: `check` already reports them as
|
|
80
|
+
* LOOKUP_UNRESOLVED with the resolution recipe.
|
|
81
|
+
*/
|
|
82
|
+
export function connectorInputErrors(rc, inputs, resolutions = {}, lookups = {}, wire = { dialect: FLOW_DIALECT }) {
|
|
83
|
+
try {
|
|
84
|
+
buildConnectorInputs(rc, inputs, { ...wire, resolutions, lookups, unresolvedLookups: 'ignore' });
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
return e instanceof Error ? e.message : String(e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function buildConnectorInputs(rc, inputs, opts) {
|
|
92
|
+
const { dialect, resolutions = {}, lookups = {}, unresolvedLookups } = opts;
|
|
93
|
+
const render = opts.render ?? ((value) => value);
|
|
94
|
+
const policy = { unresolvedLookups };
|
|
95
|
+
const fields = (rc.fields ?? []);
|
|
96
|
+
const byName = new Map(fields.map((f) => [f.name, f]));
|
|
97
|
+
const errs = validateNestedConnectorKeys(fields, inputs, byName, dialect);
|
|
98
|
+
const routed = {
|
|
99
|
+
bodyParameters: {}, pathParameters: {}, queryParameters: {},
|
|
100
|
+
};
|
|
101
|
+
// The author's values for FILE parts, by part name. The part LIST itself comes
|
|
102
|
+
// from the library (`rc.multipartParts`): a string-typed part is a transport
|
|
103
|
+
// container the runtime composes from `bodyParameters`, so it is listed without
|
|
104
|
+
// a value and is never an author input — see `readFields` in core/library.ts.
|
|
105
|
+
const partValues = new Map();
|
|
106
|
+
for (const f of fields) {
|
|
107
|
+
const flat = Object.prototype.hasOwnProperty.call(inputs, f.name);
|
|
108
|
+
let nested = flat
|
|
109
|
+
? { found: true, value: inputs[f.name], projectedArrays: 0 }
|
|
110
|
+
: readConnectorPath(inputs, f.name, dialect);
|
|
111
|
+
if (!nested.found) {
|
|
112
|
+
// A required LOOKUP field is missing far more often because the author does
|
|
113
|
+
// not know what to put in it than because they forgot it exists: name the
|
|
114
|
+
// id it sends and the helper that finds one, so "(string)" is not read as
|
|
115
|
+
// "any string" — an e-mail into `fields.reporter.id` is exactly that misreading.
|
|
116
|
+
if (f.required)
|
|
117
|
+
errs.push(`missing required input "${f.name}" (${f.type ?? 'value'})${missingLookupHint(f.name, lookups[f.name], rc, dialect.descriptors)}`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// An unresolved lookup is swapped for its recorded id, or reported. It is
|
|
121
|
+
// NOT rendered as a placeholder: emitting something for a value nobody
|
|
122
|
+
// resolved would produce a flow that compiles and then talks to the wrong
|
|
123
|
+
// record, which is exactly the failure the lookup surface exists to remove.
|
|
124
|
+
//
|
|
125
|
+
// A token is swapped wherever the field's value can hold one: the whole
|
|
126
|
+
// value, or an element of an array at any depth. A multiselect is
|
|
127
|
+
// `users: [lookup(…), lookup(…)]` — the form `ConnectorLookupValue`'s array
|
|
128
|
+
// branch types and the generated `## Example` prints — and until this swapped
|
|
129
|
+
// only the whole-value case, so every token inside the array reached
|
|
130
|
+
// validation as an object ("expected string, got object") with every lookup
|
|
131
|
+
// recorded. An unresolved element under 'ignore' becomes `null`, which
|
|
132
|
+
// validation skips, so `check` still type-checks the resolved siblings.
|
|
133
|
+
const unresolved = [];
|
|
134
|
+
const swapped = swapLookupTokens(nested.value, resolutions, unresolved);
|
|
135
|
+
if (unresolved.length) {
|
|
136
|
+
if (policy.unresolvedLookups !== 'ignore') {
|
|
137
|
+
for (const token of unresolved)
|
|
138
|
+
errs.push(unresolvedLookupMessage(token));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (isLookupToken(nested.value))
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
nested = { ...nested, value: swapped };
|
|
145
|
+
// A FLAT key whose wire name ends in `[*]` names one array: an array value
|
|
146
|
+
// there is checked element by element, as the nested form is. A flat scalar
|
|
147
|
+
// keeps depth 0 and is emitted as the scalar it is — the product wraps it
|
|
148
|
+
// into a one-element list (flow-v1 `convertDotNotationToNested`).
|
|
149
|
+
const depth = flat
|
|
150
|
+
? (f.name.endsWith('[*]') && Array.isArray(nested.value) ? 1 : 0)
|
|
151
|
+
: nested.projectedArrays;
|
|
152
|
+
validateConnectorFieldValue(f, nested.value, depth, errs, dialect);
|
|
153
|
+
if (f.location === 'multipart') {
|
|
154
|
+
partValues.set(f.name, render(nested.value));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const slot = SLOT_FOR_LOCATION[f.location ?? 'body'] ?? 'bodyParameters';
|
|
158
|
+
routed[slot][f.name] = render(nested.value);
|
|
159
|
+
}
|
|
160
|
+
if (errs.length) {
|
|
161
|
+
// Name the location of each required field: a "missing required input" on a
|
|
162
|
+
// path parameter reads very differently from one on a body field, and the
|
|
163
|
+
// author's fix is the same either way (add it to the same flat `inputs` map).
|
|
164
|
+
const req = fields
|
|
165
|
+
.filter((f) => f.required)
|
|
166
|
+
.map((f) => (f.location && f.location !== 'body' ? `${f.name} (${f.location})` : f.name))
|
|
167
|
+
.join(', ') || '(none required)';
|
|
168
|
+
// Phase 1 propagates connector discovery capability to every activity. When
|
|
169
|
+
// one of those activities rejects an unknown field, point at the live-schema
|
|
170
|
+
// preparation path; fully static activities on the same connector still
|
|
171
|
+
// compile normally and never reach this hint.
|
|
172
|
+
const schemaDynamic = !!rc.entry?.runtime?.requiresConnectionForSchema;
|
|
173
|
+
const hasUnknown = errs.some((e) => e.startsWith('unknown input'));
|
|
174
|
+
let hint = '';
|
|
175
|
+
if (schemaDynamic && hasUnknown) {
|
|
176
|
+
const prefix = `uipath.connector.${rc.connectorKey}.`;
|
|
177
|
+
const action = String(rc.nodeType).startsWith(prefix)
|
|
178
|
+
? String(rc.nodeType).slice(prefix.length)
|
|
179
|
+
// `.pop()` on a split is never empty at runtime, but its type admits
|
|
180
|
+
// `undefined`; the node type itself is the only sane fallback.
|
|
181
|
+
: String(rc.nodeType).split('.').pop() ?? String(rc.nodeType);
|
|
182
|
+
hint =
|
|
183
|
+
` This connector supports connection-specific object or field discovery, and the ` +
|
|
184
|
+
`static library does not contain this input. Run ` +
|
|
185
|
+
`\`${prepareCommand(rc.connectorKey, action)}\` ` +
|
|
186
|
+
`to load them ` +
|
|
187
|
+
`(writes ./.flow-sdk/connectors-local/, which the compiler auto-detects). If that still ` +
|
|
188
|
+
`does not carry this input, the operation resolves its fields from the VALUES of ` +
|
|
189
|
+
`parent inputs: resolve every parent and re-run with ` +
|
|
190
|
+
`\`-f <parent-field>=<value>\` for each — too few parents is refused, and a ` +
|
|
191
|
+
`wrong value resolves back to just the parents. ` +
|
|
192
|
+
`See the connector guide's parent-field loop.`;
|
|
193
|
+
}
|
|
194
|
+
throw new Error(`connector ${rc.nodeType}: ${errs.join('; ')}. Required fields: ${req}.${hint}`);
|
|
195
|
+
}
|
|
196
|
+
// Every declared part, in declaration order, as the designer lists them: the
|
|
197
|
+
// container part valueless, a file part with the author's value when given. A
|
|
198
|
+
// test double that resolves no `multipartParts` falls back to its multipart
|
|
199
|
+
// fields, which are all file parts by construction.
|
|
200
|
+
const declaredParts = rc.multipartParts
|
|
201
|
+
?? fields.filter((f) => f.location === 'multipart').map((f) => ({ name: f.name, dataType: f.type ?? 'string' }));
|
|
202
|
+
const multipartParameters = declaredParts.map((part) => (partValues.has(part.name)
|
|
203
|
+
? { name: part.name, dataType: part.dataType, value: partValues.get(part.name) }
|
|
204
|
+
: { name: part.name, dataType: part.dataType }));
|
|
205
|
+
// A FIXED slot order, so a recompile is byte-identical no matter which order
|
|
206
|
+
// the author listed the inputs in.
|
|
207
|
+
const slots = multipartParameters.length === 0 || Object.keys(routed.bodyParameters).length > 0
|
|
208
|
+
? { bodyParameters: routed.bodyParameters }
|
|
209
|
+
: {};
|
|
210
|
+
if (Object.keys(routed.pathParameters).length)
|
|
211
|
+
slots.pathParameters = routed.pathParameters;
|
|
212
|
+
if (Object.keys(routed.queryParameters).length)
|
|
213
|
+
slots.queryParameters = routed.queryParameters;
|
|
214
|
+
if (multipartParameters.length) {
|
|
215
|
+
slots.multipartParameters = multipartParameters;
|
|
216
|
+
slots.inputMetadata = { type: 'multipart', multipart: { bodyFieldName: multipartBodyFieldName(declaredParts) } };
|
|
217
|
+
}
|
|
218
|
+
return slots;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* `value` with every lookup token swapped for its recorded id: the value itself,
|
|
222
|
+
* or the elements of an array at any depth. An unrecorded token is pushed to
|
|
223
|
+
* `unresolved` and stands as `null`, so an array keeps its length.
|
|
224
|
+
*
|
|
225
|
+
* Objects are NOT descended: a lookup's place is a field's value or an element
|
|
226
|
+
* of an array field (`ConnectorLookupValue`), and a nested path such as
|
|
227
|
+
* `contacts[*].id` reaches here already projected to its leaf array. A token
|
|
228
|
+
* anywhere else is refused by {@link renderDeep}.
|
|
229
|
+
*/
|
|
230
|
+
function swapLookupTokens(value, resolutions, unresolved) {
|
|
231
|
+
if (isLookupToken(value)) {
|
|
232
|
+
const resolved = resolvedValue(value, resolutions);
|
|
233
|
+
if (resolved === undefined) {
|
|
234
|
+
unresolved.push(value);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return resolved;
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(value))
|
|
240
|
+
return value.map((item) => swapLookupTokens(item, resolutions, unresolved));
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* `inputMetadata.multipart.bodyFieldName` names the CONTAINER part — the one the
|
|
245
|
+
* runtime composes from `bodyParameters`. Measured on the designer's own nodes in
|
|
246
|
+
* the eval archive: `body` ×413, `RagRequest` ×1 (GenAI `content-generation`, whose
|
|
247
|
+
* string part is named that), and `body` on 91 of 93 file-only operations. A
|
|
248
|
+
* mis-named container is fatal at dispatch — hardcoding `body` for GenAI made
|
|
249
|
+
* Integration Service answer `400 The request body is empty` (verified live
|
|
250
|
+
* 2026-09-02) — so it is the string part's own name, with `body` only as the
|
|
251
|
+
* default for an operation that has no string part.
|
|
252
|
+
*/
|
|
253
|
+
export function multipartBodyFieldName(parts) {
|
|
254
|
+
const containers = parts.filter((p) => p.dataType.toLowerCase() !== 'file');
|
|
255
|
+
return containers.length === 1 ? containers[0].name : 'body';
|
|
256
|
+
}
|
|
257
|
+
const SLOT_FOR_LOCATION = {
|
|
258
|
+
body: 'bodyParameters',
|
|
259
|
+
query: 'queryParameters',
|
|
260
|
+
path: 'pathParameters',
|
|
261
|
+
};
|
|
262
|
+
function connectorPath(name) {
|
|
263
|
+
return name.split('.').map((raw) => ({
|
|
264
|
+
key: raw.endsWith('[*]') ? raw.slice(0, -3) : raw,
|
|
265
|
+
array: raw.endsWith('[*]'),
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
/** Read a natural nested authoring value using a registry path such as
|
|
269
|
+
* `contacts[*].id`. Wildcards project one value per array element so the Flow
|
|
270
|
+
* bodyParameters map can retain the registry's exact field ids. */
|
|
271
|
+
function readConnectorPath(inputs, name, dialect) {
|
|
272
|
+
const parts = connectorPath(name);
|
|
273
|
+
const read = (value, index) => {
|
|
274
|
+
if (index >= parts.length)
|
|
275
|
+
return { found: true, value, projectedArrays: 0 };
|
|
276
|
+
if (!value || typeof value !== 'object' || dialect.isOpaque(value) || Array.isArray(value))
|
|
277
|
+
return { found: false, projectedArrays: 0 };
|
|
278
|
+
const part = parts[index];
|
|
279
|
+
const object = value;
|
|
280
|
+
if (!Object.prototype.hasOwnProperty.call(object, part.key))
|
|
281
|
+
return { found: false, projectedArrays: 0 };
|
|
282
|
+
const next = object[part.key];
|
|
283
|
+
if (!part.array)
|
|
284
|
+
return read(next, index + 1);
|
|
285
|
+
if (!Array.isArray(next))
|
|
286
|
+
return { found: false, projectedArrays: 0 };
|
|
287
|
+
if (index === parts.length - 1)
|
|
288
|
+
return { found: true, value: next, projectedArrays: 1 };
|
|
289
|
+
const projected = next.map((item) => read(item, index + 1));
|
|
290
|
+
if (!projected.some((item) => item.found))
|
|
291
|
+
return { found: false, projectedArrays: 0 };
|
|
292
|
+
return {
|
|
293
|
+
found: true,
|
|
294
|
+
value: projected.map((item) => item.found ? item.value : null),
|
|
295
|
+
projectedArrays: 1 + Math.max(0, ...projected.map((item) => item.projectedArrays)),
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
return read(inputs, 0);
|
|
299
|
+
}
|
|
300
|
+
function buildConnectorInputTrie(fields) {
|
|
301
|
+
const root = { children: new Map(), array: false };
|
|
302
|
+
for (const field of fields) {
|
|
303
|
+
let cursor = root;
|
|
304
|
+
for (const part of connectorPath(field.name)) {
|
|
305
|
+
let child = cursor.children.get(part.key);
|
|
306
|
+
if (!child) {
|
|
307
|
+
child = { children: new Map(), array: part.array };
|
|
308
|
+
cursor.children.set(part.key, child);
|
|
309
|
+
}
|
|
310
|
+
child.array ||= part.array;
|
|
311
|
+
cursor = child;
|
|
312
|
+
}
|
|
313
|
+
cursor.terminal = field;
|
|
314
|
+
}
|
|
315
|
+
return root;
|
|
316
|
+
}
|
|
317
|
+
/** Reject nested typos with the same unknown-key message as flat keys. */
|
|
318
|
+
function validateNestedConnectorKeys(fields, inputs, exact, dialect) {
|
|
319
|
+
const root = buildConnectorInputTrie(fields);
|
|
320
|
+
const errs = [];
|
|
321
|
+
const visit = (value, node, path) => {
|
|
322
|
+
if (dialect.isOpaque(value))
|
|
323
|
+
return;
|
|
324
|
+
if (node.array) {
|
|
325
|
+
if (!Array.isArray(value)) {
|
|
326
|
+
errs.push(`input "${path}" expected array, got ${typeof value}`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
for (const item of value)
|
|
330
|
+
visit(item, { ...node, array: false }, `${path}[*]`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// An opaque object/array terminal owns its complete value; there is no
|
|
334
|
+
// child metadata with which to validate its internal keys.
|
|
335
|
+
if (node.terminal && node.children.size === 0)
|
|
336
|
+
return;
|
|
337
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
338
|
+
if (node.children.size > 0)
|
|
339
|
+
errs.push(`input "${path}" expected object, got ${Array.isArray(value) ? 'array' : typeof value}`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
343
|
+
const child = node.children.get(key);
|
|
344
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
345
|
+
if (!child) {
|
|
346
|
+
const near = nearestField(childPath, fields.map((f) => f.name));
|
|
347
|
+
errs.push(`unknown input "${childPath}"${near ? ` — did you mean "${near}"?` : ''}`);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
visit(childValue, child, childPath);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
355
|
+
if (exact.has(key))
|
|
356
|
+
continue; // backwards-compatible flat authoring
|
|
357
|
+
const node = root.children.get(key);
|
|
358
|
+
if (!node) {
|
|
359
|
+
const near = nearestField(key, [...exact.keys()]);
|
|
360
|
+
errs.push(`unknown input "${key}"${near ? ` — did you mean "${near}"?` : ''}`);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
visit(value, node, key);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return errs;
|
|
367
|
+
}
|
|
368
|
+
function validateConnectorFieldValue(field, value, projectedArrays, errs, dialect) {
|
|
369
|
+
if (projectedArrays > 0) {
|
|
370
|
+
if (!Array.isArray(value)) {
|
|
371
|
+
errs.push(`input "${field.name}" expected array projection, got ${typeof value}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
for (const item of value) {
|
|
375
|
+
if (item !== null && item !== undefined)
|
|
376
|
+
validateConnectorFieldValue(field, item, projectedArrays - 1, errs, dialect);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// An opaque value is whatever the family cannot evaluate here — a Flow `Expr`,
|
|
381
|
+
// a BPMN `=`-expression. Its runtime type is unknowable, so it is not checked.
|
|
382
|
+
if (dialect.isOpaque(value) || value === null || value === undefined)
|
|
383
|
+
return;
|
|
384
|
+
if (field.type === 'array') {
|
|
385
|
+
if (!Array.isArray(value))
|
|
386
|
+
errs.push(`input "${field.name}" expected array, got ${typeof value}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (field.type === 'object') {
|
|
390
|
+
if (typeof value !== 'object' || Array.isArray(value))
|
|
391
|
+
errs.push(`input "${field.name}" expected object, got ${Array.isArray(value) ? 'array' : typeof value}`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (field.type === 'string' || field.type === 'number' || field.type === 'boolean') {
|
|
395
|
+
const actual = typeof value;
|
|
396
|
+
const ok = actual === field.type ||
|
|
397
|
+
(field.type === 'number' && actual === 'string' && value !== '' && !isNaN(Number(value)));
|
|
398
|
+
if (!ok)
|
|
399
|
+
errs.push(`input "${field.name}" expected ${field.type}, got ${actual}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* The closest declared field name to `key`, for the "did you mean …?" half of an
|
|
404
|
+
* unknown-input error.
|
|
405
|
+
*
|
|
406
|
+
* Three rules, tried in order: an exact case-insensitive match (`pagesize` →
|
|
407
|
+
* `pageSize`), a substring either way (`summary` → `fields.summary`), and — the
|
|
408
|
+
* one added for query/path parameters — a small EDIT DISTANCE. The first two miss
|
|
409
|
+
* every substitution typo: `projekt` and `project` share no containment, so the
|
|
410
|
+
* suggestion was simply absent, and it is absent exactly where it is needed most.
|
|
411
|
+
* Parameter names are unfamiliar (`issuetype`, `nextPage`, `expansionLevel`), so
|
|
412
|
+
* the author has no memory to fall back on.
|
|
413
|
+
*
|
|
414
|
+
* The threshold scales with the name's length (⌈len/4⌉, at least 1, at most 3), so
|
|
415
|
+
* a short name needs a near-exact miss and a long one may be off by a few — and
|
|
416
|
+
* two genuinely different fields are never conflated.
|
|
417
|
+
*/
|
|
418
|
+
function nearestField(key, names) {
|
|
419
|
+
const lk = key.toLowerCase();
|
|
420
|
+
const exact = names.find((n) => n.toLowerCase() === lk);
|
|
421
|
+
if (exact)
|
|
422
|
+
return exact;
|
|
423
|
+
const contained = names.find((n) => n.toLowerCase().includes(lk) || lk.includes(n.toLowerCase()));
|
|
424
|
+
if (contained)
|
|
425
|
+
return contained;
|
|
426
|
+
const budget = Math.min(3, Math.max(1, Math.ceil(lk.length / 4)));
|
|
427
|
+
let best;
|
|
428
|
+
let bestD = budget + 1;
|
|
429
|
+
for (const n of names) {
|
|
430
|
+
const d = editDistance(lk, n.toLowerCase());
|
|
431
|
+
if (d < bestD) {
|
|
432
|
+
bestD = d;
|
|
433
|
+
best = n;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return bestD <= budget ? best : undefined;
|
|
437
|
+
}
|
|
438
|
+
/** Levenshtein distance, two rows — enough for field-name suggestions. */
|
|
439
|
+
function editDistance(a, b) {
|
|
440
|
+
if (a === b)
|
|
441
|
+
return 0;
|
|
442
|
+
if (Math.abs(a.length - b.length) > 3)
|
|
443
|
+
return 99; // cheap bail: beyond any budget
|
|
444
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
445
|
+
for (let i = 1; i <= a.length; i++) {
|
|
446
|
+
const row = [i];
|
|
447
|
+
for (let j = 1; j <= b.length; j++) {
|
|
448
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
449
|
+
}
|
|
450
|
+
prev = row;
|
|
451
|
+
}
|
|
452
|
+
return prev[b.length];
|
|
453
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/event-checks — the connector-EVENT rules, shared by every family that can
|
|
3
|
+
* subscribe to one.
|
|
4
|
+
*
|
|
5
|
+
* The activity twin is `core/connector-checks.ts`. These were Flow's, and like
|
|
6
|
+
* those they were never flow-shaped: both functions take the subscription as
|
|
7
|
+
* `unknown` and duck-type `{ connector, event, where, object, filters, version,
|
|
8
|
+
* connection }`, because that is all they need. BPMN's `.eventTrigger()` /
|
|
9
|
+
* `.waitForEvent()` node carries the same facts under its own names, and
|
|
10
|
+
* `bpmn/check.ts` adapts one to the other.
|
|
11
|
+
*
|
|
12
|
+
* What BPMN does NOT carry is `filters` — payload filtering is a Flow surface —
|
|
13
|
+
* so the `EVENT_FILTER_*` family simply does not fire there. The rules that do
|
|
14
|
+
* are the ones about the SUBSCRIPTION: whether it resolves at all, whether a
|
|
15
|
+
* generic event named its object, and whether its scope names parameters the
|
|
16
|
+
* operation actually declares. `skill-bpmn-outlook-waitfor-email` in
|
|
17
|
+
* `flow-v2-run-8` failed on exactly that last question.
|
|
18
|
+
*/
|
|
19
|
+
import { type LookupResolutions } from './lookups.js';
|
|
20
|
+
import { type ByoaEventFacts, type ByoaLedger } from './byoa.js';
|
|
21
|
+
import type { BindingsView } from './binding-messages.js';
|
|
22
|
+
/** A code/message sink, as each family's check supplies one. */
|
|
23
|
+
export type ReportEvent = (code: string, message: string, suggestion?: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* What a connector operation declares as its EVENT PARAMETERS — the `where` half
|
|
26
|
+
* of a subscription. `known: false` means "no library was supplied", which is a
|
|
27
|
+
* different answer from `known: true, names: []` ("this operation takes none").
|
|
28
|
+
* Conflating the two is what made `EVENT_NO_SCOPE` an error for the HTTP Webhook
|
|
29
|
+
* connector, whose only correct `where` is empty.
|
|
30
|
+
*/
|
|
31
|
+
export interface EventParamFields {
|
|
32
|
+
known: boolean;
|
|
33
|
+
names: string[];
|
|
34
|
+
/**
|
|
35
|
+
* The operation is a GENERIC event (`activityType: GenericTrigger`): its node type
|
|
36
|
+
* covers every object of the connection, so the subscription must name one with
|
|
37
|
+
* `object`. Unknown (no library) reads as `false`, which fails open.
|
|
38
|
+
*/
|
|
39
|
+
generic?: boolean;
|
|
40
|
+
/** The entry's own object — `''` for a generic event, the built-in one for a curated event. */
|
|
41
|
+
object?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Required parameters that SELECT the schema rather than scope the
|
|
44
|
+
* subscription (Jira's `project` / `issuetype`). Their values decide which
|
|
45
|
+
* filter and output fields exist, and no CLI surface accepts them — so when
|
|
46
|
+
* this is non-empty the known filter vocabulary is a BASELINE, and a leaf
|
|
47
|
+
* outside it may still be legal.
|
|
48
|
+
*/
|
|
49
|
+
selectors?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* The named object has no prepared variant on a connector whose objects come
|
|
52
|
+
* from the connection — the generic template answered instead.
|
|
53
|
+
*/
|
|
54
|
+
objectUnprepared?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The connector discovers custom fields per connection and nothing has
|
|
57
|
+
* prepared this event, so the known vocabulary is a strict subset of the
|
|
58
|
+
* tenant's BY DESIGN.
|
|
59
|
+
*/
|
|
60
|
+
customFieldsUnprepared?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The filter vocabulary is a subset that NO prepare can complete.
|
|
63
|
+
*
|
|
64
|
+
* An activity closes this gap: `resources describe` takes `-f name=value`
|
|
65
|
+
* and `--action`, so supplying the parent values runs the connector's
|
|
66
|
+
* ObjectAction (Jira `GenerateSchema`, Salesforce `GenerateQuerySchema`) and
|
|
67
|
+
* the custom fields come back. `uip is triggers describe` has neither flag,
|
|
68
|
+
* so for an EVENT on a custom-field-discovering connector there is no call
|
|
69
|
+
* that reaches the parent-scoped fields — the designer grows Jira's filter
|
|
70
|
+
* list once a project and issue type are picked, and nothing in this CLI
|
|
71
|
+
* can. Distinct from `customFieldsUnprepared`, which preparing DOES fix.
|
|
72
|
+
*/
|
|
73
|
+
filtersPartial?: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** The library-backed facts the event rules need, or `undefined` for "no library". */
|
|
76
|
+
export interface EventCheckOpts {
|
|
77
|
+
/** A connector event's declared event PARAMETERS — the `where` half of a subscription. */
|
|
78
|
+
eventParams?: (connector: string, event: string, object?: string) => EventParamFields | undefined;
|
|
79
|
+
/** What `compile` would refuse a subscription with, or `undefined` when it resolves. */
|
|
80
|
+
eventResolveError?: (kind: 'trigger' | 'event', subscription: {
|
|
81
|
+
connector: string;
|
|
82
|
+
event: string;
|
|
83
|
+
version?: string;
|
|
84
|
+
object?: string;
|
|
85
|
+
}) => string | undefined;
|
|
86
|
+
/** A connector event's FILTERABLE field names — the vocabulary a `filters` leaf may name. */
|
|
87
|
+
eventFilterFields?: (connector: string, event: string, object?: string) => string[] | undefined;
|
|
88
|
+
/** A connector event's BYOA and webhook-URL facts, or `undefined` when unknown. */
|
|
89
|
+
eventFacts?: (connector: string, event: string, object?: string) => ByoaEventFacts | undefined;
|
|
90
|
+
/** The BYOA checks `registry prepare` recorded. */
|
|
91
|
+
byoa?: ByoaLedger;
|
|
92
|
+
/** Everything `registry prepare --resolve` has recorded in the local overlay. */
|
|
93
|
+
resolutions?: LookupResolutions;
|
|
94
|
+
/** The project's `bindings.json`, for the BYOA connection check. */
|
|
95
|
+
bindings?: BindingsView;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Ask the library what event parameters a subscription's operation declares.
|
|
99
|
+
* Returns `undefined` when the subscription is too broken to look up (its own
|
|
100
|
+
* diagnostics say so) or when no resolver was supplied — both mean "unknown", and
|
|
101
|
+
* unknown fails open.
|
|
102
|
+
*/
|
|
103
|
+
export declare function eventParamsFor(subscription: unknown, opts: EventCheckOpts): EventParamFields | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* The event-side resolution rules — the trigger twin of `checkConnectorLookups`,
|
|
106
|
+
* plus three rules with no activity counterpart:
|
|
107
|
+
*
|
|
108
|
+
* - `EVENT_UNKNOWN` — an ERROR: a library is loaded and it cannot resolve the
|
|
109
|
+
* subscription, so `compile` will refuse it. The message is the resolver's
|
|
110
|
+
* own (`eventResolveError`), the same text compile throws. Checked first; when
|
|
111
|
+
* it fires, EVENT_UNPREPARED and the filter rules below are skipped for this
|
|
112
|
+
* subscription, since there is no entry for them to read.
|
|
113
|
+
* - `EVENT_UNPREPARED` — the event RESOLVES but the library knows nothing
|
|
114
|
+
* about its parameters (or no `eventResolveError` resolver was supplied):
|
|
115
|
+
* the fallback case for a connector the shipped library was not generated
|
|
116
|
+
* against. A warning, because compile still succeeds. Prints the one
|
|
117
|
+
* command that makes the event checkable.
|
|
118
|
+
* - `EVENT_BYOA_UNVERIFIED` — an ERROR: the event only fires on a BYOA
|
|
119
|
+
* (bring-your-own-app) connection, and the bound connection id has no record
|
|
120
|
+
* of `prepare` having checked it (`core/byoa.ts` has the rule and when it
|
|
121
|
+
* stays silent). A standard connection deploys and the trigger never fires;
|
|
122
|
+
* the usual way in is binding the connection by preparing one of the
|
|
123
|
+
* connector's ACTIONS, which checks nothing about BYOA. `compile` throws the
|
|
124
|
+
* same text.
|
|
125
|
+
* - `EVENT_FILTER_UNKNOWN_FIELD` — a filter leaf naming a field outside the
|
|
126
|
+
* prepared vocabulary is an ERROR, not a warning, because the platform's own
|
|
127
|
+
* configure DROPS an unmatched leaf and reports Success: the deployed
|
|
128
|
+
* trigger then fires on events the filter should exclude, silently. Matching
|
|
129
|
+
* is case-sensitive, so the near-miss hint checks case first. When the
|
|
130
|
+
* vocabulary is unknown or known-empty this stays quiet — fail open.
|
|
131
|
+
*
|
|
132
|
+
* Unresolved `where` lookups follow the activity rule exactly: a WARNING here
|
|
133
|
+
* (compile repeats it as the hard failure), with the recorded answer looked up
|
|
134
|
+
* under the event-form node type — the form `prepare` stores.
|
|
135
|
+
*/
|
|
136
|
+
export declare function checkEventResolution(subscription: unknown, where: string, kind: 'trigger' | 'event', opts: EventCheckOpts, error: (code: string, message: string, suggestion?: string) => void, warn: (code: string, message: string, suggestion?: string) => void, eventParams?: EventParamFields): void;
|
|
137
|
+
export declare function checkEventSubscription(subscription: unknown, where: string, at: (code: string, message: string, suggestion?: string) => void, warn?: (code: string, message: string, suggestion?: string) => void, eventParams?: EventParamFields): void;
|