@uipath/maestro-builder-sdk 6.70.9 → 6.71.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/check.js CHANGED
@@ -571,17 +571,6 @@ function checkFlow(built, ancestors, opts = {}) {
571
571
  outputReaders.set(stepName, (field, rest, at, d) => rule(stepName, field, rest, inputs, at, d));
572
572
  }
573
573
  }
574
- if (s.spec.kind === 'waitEvent') {
575
- const stepName = s.name;
576
- const push = (level) => (code, message, suggestion) => diags.push({
577
- level, code, message, step: stepName,
578
- ...(suggestion ? { suggestion } : {}),
579
- });
580
- // What compile refuses (the event itself, its filters, its binding
581
- // labels) is the serializer's, reported from its run.
582
- checkEventSubscription(s.spec.subscription, `waitForEvent "${stepName}"`, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts), false);
583
- checkEventResolution(s.spec.subscription, `waitForEvent "${stepName}"`, 'event', opts, push('error'), push('warning'), eventParamsFor(s.spec.subscription, opts), false);
584
- }
585
574
  if (s.spec.kind === 'subflow') {
586
575
  // Only a real child goes in the map: a missing one is reported by
587
576
  // checkSubflow, and recursing into `undefined` would crash the check
@@ -2419,6 +2408,17 @@ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf,
2419
2408
  options: opts,
2420
2409
  bindingLabels: (where, labels, _use, target) => checkBindingLabels(where, labels, opts, (code, message) => diags.push({ level: 'error', code, step: s.name, message }), target),
2421
2410
  resources: { derivedNodeId: () => { }, claimNodeId: () => undefined, claimFolder: () => undefined },
2411
+ eventSubscription: (subscription, where) => {
2412
+ const push = (level) => (code, message, suggestion) => diags.push({
2413
+ level, code, message, step: s.name,
2414
+ ...(suggestion ? { suggestion } : {}),
2415
+ });
2416
+ // What compile refuses (the event itself, its filters, its binding
2417
+ // labels) is the serializer's, reported from its run.
2418
+ const params = eventParamsFor(subscription, opts);
2419
+ checkEventSubscription(subscription, where, push('error'), push('warning'), params, false);
2420
+ checkEventResolution(subscription, where, 'event', opts, push('error'), push('warning'), params, false);
2421
+ },
2422
2422
  // The operation, its field contract and its binding labels are the
2423
2423
  // serializer's (CONNECTOR_*, BINDING_*), reported from its run.
2424
2424
  connector: (c) => {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * An Integration Service event subscription as Flow emits it — the node body an
3
+ * event trigger (`onEvent`, `.entryPoint()`), and a mid-flow wait
4
+ * (`waitForEvent`), share: `inputs.detail`, the library's resolution of the
5
+ * event, and the refusals compile makes before and without a library.
6
+ *
7
+ * Moved from serialize.ts in Phase 4 of docs/NODE_TYPE_ARCHITECTURE_DESIGN.md,
8
+ * so the connector family's event wait and the triggers that serialize still
9
+ * places read one copy. What serialize held in `SerializeOpts` comes in as an
10
+ * {@link EventEmitEnv}: the library, the bindings, and how a binding label
11
+ * resolves.
12
+ */
13
+ import type { EventSubscription } from './actions.js';
14
+ import { type BindingsView, type PrepareTarget } from './binding-messages.js';
15
+ import { CompileRefusal } from './compile-refusal.js';
16
+ import { readEventFilter } from './event-filters.js';
17
+ import type { Library, ResolvedConnector } from './library.js';
18
+ /** What an event subscription's emission needs from the compile: the library, the bindings, and how a label resolves. */
19
+ export interface EventEmitEnv {
20
+ readonly library?: Library;
21
+ readonly bindings?: BindingsView;
22
+ /** A connection or folder label to the key the node carries, with compile's refusals and warnings. */
23
+ resolveBinding(name: string | undefined, attribute: 'connection' | 'folder', where: string, target?: PrepareTarget): string | undefined;
24
+ }
25
+ /** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
26
+ export declare function eventTarget(sub: {
27
+ connector?: unknown;
28
+ event?: unknown;
29
+ }): PrepareTarget | undefined;
30
+ /**
31
+ * Throw `check`'s EVENT_BYOA_UNVERIFIED for a subscription whose event only
32
+ * fires on a BYOA connection and whose bound connection `prepare` never checked
33
+ * — the one condition in `core/byoa.ts`, fed from the same library and
34
+ * bindings, so a flow check refuses never compiles.
35
+ *
36
+ * `where` is check's spelling of the site (`trigger` for `onEvent`, entry points
37
+ * included; `waitForEvent "<step>"`), so the two print the same sentence.
38
+ */
39
+ export declare function refuseUnverifiedByoa(env: EventEmitEnv, where: string, sub: {
40
+ connector: string;
41
+ event: string;
42
+ object?: string;
43
+ connection?: unknown;
44
+ }): void;
45
+ /**
46
+ * Refuse a connector-bound node (connector step, event trigger, `.entryPoint`
47
+ * onEvent, `waitForEvent`) that names no `connection` or no `folder` label.
48
+ * (An inline agent's connector tool is #875's: `check` refuses it as
49
+ * INLINE_AGENT_TOOL_CONNECTOR_NO_CONNECTION, and serialize leaves it unbound.)
50
+ * `resolveBinding` returns `undefined` for an absent label, which used to reach
51
+ * the node as `detail.connectionId: null` with no binding — valid to compile,
52
+ * refused by `flow validate` (flow-builder-sdk#890): BINDING_MISSING.
53
+ */
54
+ export declare function requireBindingPair(where: string, labels: {
55
+ connection?: unknown;
56
+ folder?: unknown;
57
+ }, target?: PrepareTarget): void;
58
+ /**
59
+ * `library.resolveEvent` for one subscription, or EVENT_UNKNOWN: whatever it
60
+ * throws (a connector or event the library does not carry, a version or object
61
+ * it does not resolve) is that one condition, an event compile cannot place.
62
+ * `where` names the site as `check` always has (`trigger`, `waitForEvent "x"`).
63
+ */
64
+ export declare function resolveEvent(library: Library, kind: 'trigger' | 'event', sub: EventSubscription, where: string): ReturnType<Library['resolveEvent']>;
65
+ /**
66
+ * LIBRARY_REQUIRED for an event subscription at `site`, after the refusals that
67
+ * need no library: its binding labels, judged as `eventDetail` judges them, so a
68
+ * run without a library (`check`'s inner loop) still hears about them. The
69
+ * refusal carries the connection the node is bound to, so its collect-mode
70
+ * stand-in still takes part in the file-wide connection rule (`connectionConnectors`).
71
+ */
72
+ export declare function libraryRequiredForEvent(env: EventEmitEnv, sub: EventSubscription, nodeId: string, site: string, call: string): CompileRefusal;
73
+ /**
74
+ * Flatten a nested `where` onto the DOTTED parameter names the wire uses.
75
+ *
76
+ * The generated `…Where` interface splits a dotted event parameter into an
77
+ * object — `channelData.channel.id` becomes
78
+ * `channelData: ConnectorValue<ChannelDataInput>` — so an author following the
79
+ * types writes the nested shape, while `bucketOf` and the `filterExpression`
80
+ * clause both key on the declared dotted name. Without this the nested shape
81
+ * missed the bucket lookup and then stringified, emitting
82
+ * `channelData == '[object Object]'`: a subscription matching nothing, from a
83
+ * document that compiled clean and passed `check`.
84
+ *
85
+ * Descent STOPS as soon as a path names a declared parameter, so a parameter
86
+ * that genuinely takes an object value is passed through whole rather than
87
+ * shredded. A lookup token is a value, never a branch.
88
+ */
89
+ export declare function flattenWhere(where: Record<string, unknown>, declared: ReadonlySet<string>): Record<string, unknown>;
90
+ /**
91
+ * A subscription's `filters`, each leaf read structurally (`readEventFilter`),
92
+ * or the refusal of the first leaf compile cannot lower: no field, no operator
93
+ * or two, a value no operator takes, a backslash JMESPath engines read apart.
94
+ * Needs no library, so a run without one judges the leaves too
95
+ * (`libraryRequiredForEvent`).
96
+ */
97
+ export declare function eventFilterReads(filters: readonly unknown[], nodeId: string): Array<NonNullable<ReturnType<typeof readEventFilter>['read']>>;
98
+ /**
99
+ * The match-expression clauses of a subscription's EVENT parameters, one
100
+ * `k == '<value>'` per value, or the refusal of the first value compile cannot
101
+ * write there. The clause compares against a JMESPath string, so the value must
102
+ * be a string, number or boolean: an object or array would be stringified to
103
+ * `'[object Object]'` (or a comma list), a subscription that matches nothing
104
+ * (EVENT_WHERE_NOT_SCALAR). A string with a backslash JMESPath engines read
105
+ * apart is EVENT_FILTER_BACKSLASH. An absent or empty value (`''` or `[]`, so a
106
+ * `where` built from a possibly-empty list) writes no clause.
107
+ *
108
+ * Only event parameters fold into the expression; a query or path parameter is
109
+ * sent with the request that registers the subscription and is never read as
110
+ * JMESPath, so neither rule applies to it.
111
+ */
112
+ export declare function eventScopeClauses(eventParameters: Record<string, unknown>, nodeId: string): string[];
113
+ export declare function eventDetail(rc: ResolvedConnector, sub: EventSubscription, nodeId: string, env: EventEmitEnv, site: string): Record<string, unknown>;
@@ -0,0 +1,356 @@
1
+ import { BindingRefusal, bindingMissingMessage, missingBindingLabels } from './binding-messages.js';
2
+ import { ByoaRefusal, byoaUnverified } from './byoa.js';
3
+ import { CompileRefusal, LibraryRequiredRefusal } from './compile-refusal.js';
4
+ import { genericEventObjectRefusal } from './event-checks.js';
5
+ import { EVENT_FILTER_OPERATORS, eventFilterJmes, eventFilterProblem, eventFilterTreeLeaf, jmesString, jmesStringProblem, readEventFilter, } from './event-filters.js';
6
+ import { isLookupToken, resolveEventWhere } from './lookups.js';
7
+ import { stableId } from './stable-id.js';
8
+ /** What `registry prepare` takes for an event subscription: its connector key and curated event name. */
9
+ export function eventTarget(sub) {
10
+ return typeof sub.connector === 'string' && sub.connector !== ''
11
+ ? { key: sub.connector, ...(typeof sub.event === 'string' && sub.event !== '' ? { action: sub.event } : {}) }
12
+ : undefined;
13
+ }
14
+ /**
15
+ * Throw `check`'s EVENT_BYOA_UNVERIFIED for a subscription whose event only
16
+ * fires on a BYOA connection and whose bound connection `prepare` never checked
17
+ * — the one condition in `core/byoa.ts`, fed from the same library and
18
+ * bindings, so a flow check refuses never compiles.
19
+ *
20
+ * `where` is check's spelling of the site (`trigger` for `onEvent`, entry points
21
+ * included; `waitForEvent "<step>"`), so the two print the same sentence.
22
+ */
23
+ export function refuseUnverifiedByoa(env, where, sub) {
24
+ const library = env.library;
25
+ if (!library || typeof library.eventFacts !== 'function')
26
+ return;
27
+ const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
28
+ const message = byoaUnverified(where, { connector: sub.connector, event: sub.event, ...(object ? { object } : {}), connection: sub.connection }, library.eventFacts(sub.connector, sub.event, object), library.byoaVerifications, env.bindings);
29
+ if (message)
30
+ throw new ByoaRefusal(message);
31
+ }
32
+ /**
33
+ * Refuse a connector-bound node (connector step, event trigger, `.entryPoint`
34
+ * onEvent, `waitForEvent`) that names no `connection` or no `folder` label.
35
+ * (An inline agent's connector tool is #875's: `check` refuses it as
36
+ * INLINE_AGENT_TOOL_CONNECTOR_NO_CONNECTION, and serialize leaves it unbound.)
37
+ * `resolveBinding` returns `undefined` for an absent label, which used to reach
38
+ * the node as `detail.connectionId: null` with no binding — valid to compile,
39
+ * refused by `flow validate` (flow-builder-sdk#890): BINDING_MISSING.
40
+ */
41
+ export function requireBindingPair(where, labels, target) {
42
+ const missing = missingBindingLabels(labels);
43
+ if (missing.length > 0)
44
+ throw new BindingRefusal('BINDING_MISSING', bindingMissingMessage(where, missing, target));
45
+ }
46
+ /**
47
+ * `library.resolveEvent` for one subscription, or EVENT_UNKNOWN: whatever it
48
+ * throws (a connector or event the library does not carry, a version or object
49
+ * it does not resolve) is that one condition, an event compile cannot place.
50
+ * `where` names the site as `check` always has (`trigger`, `waitForEvent "x"`).
51
+ */
52
+ export function resolveEvent(library, kind, sub, where) {
53
+ try {
54
+ return library.resolveEvent(kind, sub.connector, sub.event, sub.version, sub.object);
55
+ }
56
+ catch (error) {
57
+ if (error instanceof Error && error.message.startsWith('internal SDK error:'))
58
+ throw error;
59
+ throw new CompileRefusal('EVENT_UNKNOWN', `${where}: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ }
62
+ /**
63
+ * LIBRARY_REQUIRED for an event subscription at `site`, after the refusals that
64
+ * need no library: its binding labels, judged as `eventDetail` judges them, so a
65
+ * run without a library (`check`'s inner loop) still hears about them. The
66
+ * refusal carries the connection the node is bound to, so its collect-mode
67
+ * stand-in still takes part in the file-wide connection rule (`connectionConnectors`).
68
+ */
69
+ export function libraryRequiredForEvent(env, sub, nodeId, site, call) {
70
+ let use;
71
+ if (sub !== null && typeof sub === 'object') {
72
+ // With no library no parameter is declared, so every `where` key lands in
73
+ // eventParameters and folds into the match expression, exactly as
74
+ // `eventDetail` places an undeclared key. A lookup token is a value only
75
+ // a library resolves; it is not judged here.
76
+ if (sub.where !== null && typeof sub.where === 'object') {
77
+ const scope = Object.fromEntries(Object.entries(sub.where).filter(([, v]) => !isLookupToken(v)));
78
+ eventScopeClauses(flattenWhere(scope, new Set()), nodeId);
79
+ }
80
+ if (Array.isArray(sub.filters))
81
+ eventFilterReads(sub.filters, nodeId);
82
+ requireBindingPair(site, sub, eventTarget(sub));
83
+ const connection = env.resolveBinding(sub.connection, 'connection', `event node "${nodeId}"`, eventTarget(sub));
84
+ env.resolveBinding(sub.folder, 'folder', `event node "${nodeId}"`, eventTarget(sub));
85
+ const connector = eventTarget(sub)?.key;
86
+ if (connection !== undefined && connector !== undefined)
87
+ use = { connector, connection };
88
+ }
89
+ return new LibraryRequiredRefusal(`${call} needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`, use);
90
+ }
91
+ /**
92
+ * Flatten a nested `where` onto the DOTTED parameter names the wire uses.
93
+ *
94
+ * The generated `…Where` interface splits a dotted event parameter into an
95
+ * object — `channelData.channel.id` becomes
96
+ * `channelData: ConnectorValue<ChannelDataInput>` — so an author following the
97
+ * types writes the nested shape, while `bucketOf` and the `filterExpression`
98
+ * clause both key on the declared dotted name. Without this the nested shape
99
+ * missed the bucket lookup and then stringified, emitting
100
+ * `channelData == '[object Object]'`: a subscription matching nothing, from a
101
+ * document that compiled clean and passed `check`.
102
+ *
103
+ * Descent STOPS as soon as a path names a declared parameter, so a parameter
104
+ * that genuinely takes an object value is passed through whole rather than
105
+ * shredded. A lookup token is a value, never a branch.
106
+ */
107
+ export function flattenWhere(where, declared) {
108
+ const out = {};
109
+ const walk = (value, path) => {
110
+ if (path && declared.has(path)) {
111
+ out[path] = value;
112
+ return;
113
+ }
114
+ const plain = typeof value === 'object' && value !== null && !Array.isArray(value)
115
+ && !isLookupToken(value)
116
+ && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
117
+ if (plain) {
118
+ for (const [k, v] of Object.entries(value))
119
+ walk(v, path ? `${path}.${k}` : k);
120
+ return;
121
+ }
122
+ if (path)
123
+ out[path] = value;
124
+ };
125
+ walk(where, '');
126
+ return out;
127
+ }
128
+ /**
129
+ * A subscription's `filters`, each leaf read structurally (`readEventFilter`),
130
+ * or the refusal of the first leaf compile cannot lower: no field, no operator
131
+ * or two, a value no operator takes, a backslash JMESPath engines read apart.
132
+ * Needs no library, so a run without one judges the leaves too
133
+ * (`libraryRequiredForEvent`).
134
+ */
135
+ export function eventFilterReads(filters, nodeId) {
136
+ const operators = Object.keys(EVENT_FILTER_OPERATORS).join(', ');
137
+ const at = `event node "${nodeId}": filters`;
138
+ return filters.map((f, i) => {
139
+ const { keys, read } = readEventFilter(f);
140
+ const field = f?.field;
141
+ if (typeof field !== 'string' || field === '') {
142
+ throw new CompileRefusal('EVENT_FILTER_NO_FIELD', `${at}[${i}] has no \`field\` — name a field on the event payload, e.g. 'subject'.`);
143
+ }
144
+ if (keys.length === 0) {
145
+ throw new CompileRefusal('EVENT_FILTER_NO_VALUE', `${at}[${i}] has no operator. Write exactly one of ${operators} with its value — `
146
+ + `\`contains\` is a case-sensitive substring test, the same one Integration Service applies `
147
+ + `server-side; \`lessThan\`/\`greaterThan\` order a number or an ISO date.`, `{ field: '${field}', contains: '<text>' }`);
148
+ }
149
+ if (keys.length > 1) {
150
+ throw new CompileRefusal('EVENT_FILTER_MANY_OPERATORS', `${at}[${i}] carries ${keys.length} operators (${keys.join(', ')}); a condition has exactly `
151
+ + `one. Write two conditions instead — they are ANDed.`);
152
+ }
153
+ if (!read) {
154
+ throw new CompileRefusal('EVENT_FILTER_NO_VALUE', `${at}[${i}] gives \`${keys[0]}\` a value that is not a string, number or boolean.`);
155
+ }
156
+ // An ambiguous backslash first: `eventFilterProblem` would report it too.
157
+ const problem = eventFilterProblem(read);
158
+ const backslash = typeof read.value === 'string' && jmesStringProblem(read.value) !== undefined;
159
+ if (problem) {
160
+ throw new CompileRefusal(backslash ? 'EVENT_FILTER_BACKSLASH' : 'EVENT_FILTER_BAD_VALUE', `${at}[${i}] on \`${read.field}\`: ${problem}`);
161
+ }
162
+ return read;
163
+ });
164
+ }
165
+ /**
166
+ * The match-expression clauses of a subscription's EVENT parameters, one
167
+ * `k == '<value>'` per value, or the refusal of the first value compile cannot
168
+ * write there. The clause compares against a JMESPath string, so the value must
169
+ * be a string, number or boolean: an object or array would be stringified to
170
+ * `'[object Object]'` (or a comma list), a subscription that matches nothing
171
+ * (EVENT_WHERE_NOT_SCALAR). A string with a backslash JMESPath engines read
172
+ * apart is EVENT_FILTER_BACKSLASH. An absent or empty value (`''` or `[]`, so a
173
+ * `where` built from a possibly-empty list) writes no clause.
174
+ *
175
+ * Only event parameters fold into the expression; a query or path parameter is
176
+ * sent with the request that registers the subscription and is never read as
177
+ * JMESPath, so neither rule applies to it.
178
+ */
179
+ export function eventScopeClauses(eventParameters, nodeId) {
180
+ return Object.entries(eventParameters)
181
+ .filter(([, v]) => v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && v.length === 0))
182
+ .map(([k, v]) => {
183
+ if (typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean') {
184
+ throw new CompileRefusal('EVENT_WHERE_NOT_SCALAR', `event node "${nodeId}": \`where.${k}\` is ${Array.isArray(v) ? 'an array' : isLookupToken(v) ? 'an unresolved lookup() nested inside an object' : 'an object'}, `
185
+ + `and an event parameter is matched as one text value (\`${k} == '<value>'\`): written as it is, `
186
+ + `the subscription would compare against ${JSON.stringify(String(v))} and match nothing. `
187
+ + `Give \`${k}\` a string, number or boolean.`, `where: { '${k}': '<value>' }`);
188
+ }
189
+ const problem = jmesStringProblem(String(v));
190
+ if (problem)
191
+ throw new CompileRefusal('EVENT_FILTER_BACKSLASH', `event node "${nodeId}": \`where.${k}\`: ${problem}`);
192
+ return `${k} == ${jmesString(String(v))}`;
193
+ });
194
+ }
195
+ export function eventDetail(rc, sub, nodeId, env, site) {
196
+ requireBindingPair(site, sub, eventTarget(sub));
197
+ const conn = env.resolveBinding(sub.connection, 'connection', `event node "${nodeId}"`, eventTarget(sub));
198
+ const folder = env.resolveBinding(sub.folder, 'folder', `event node "${nodeId}"`, eventTarget(sub));
199
+ const op = (rc.entry?.operation ?? {});
200
+ const filters = sub.filters ?? [];
201
+ // A GENERIC event with no `object` would carry `objectName: ""`: it validates
202
+ // and the platform's dispatcher refuses it. Refused here, at the layer that
203
+ // would otherwise emit it, with the text every family's compile shares.
204
+ const objectName = String(op.objectName ?? '');
205
+ const noObject = genericEventObjectRefusal(rc, sub, (o) => `onEvent(…, { object: '${o}' })`);
206
+ if (noObject) {
207
+ // The object an author put in `where` by mistake is the value to move.
208
+ const scopeObject = sub.where && typeof sub.where === 'object'
209
+ ? Object.entries(sub.where).find(([k]) => /^(objectName|entityName|object)$/i.test(k))?.[1]
210
+ : undefined;
211
+ throw new CompileRefusal('EVENT_GENERIC_NO_OBJECT', `event node "${nodeId}": ${noObject}`, `object: '${scopeObject !== undefined ? String(scopeObject) : '<Object>'}'`);
212
+ }
213
+ // An unresolved `where` lookup is swapped for its recorded id — the id
214
+ // `prepare --resolve` stored under the EVENT-form node type — or refused.
215
+ // Same rule as connector inputs: emitting a placeholder for a value nobody
216
+ // resolved would subscribe the trigger to the wrong folder/channel/queue.
217
+ const where = resolveEventWhere(sub.where, env.library?.lookupResolutions ?? {}, `event node "${nodeId}"`);
218
+ // A subscription's parameters do not all travel in the same slot. The
219
+ // connector declares which, and the library bakes it per field as `bucket`:
220
+ // measured against designer-authored triggers, Jira's `project`/`issuetype`
221
+ // and GitHub's `repo` land in `queryParameters`, Asana's `projectId` in
222
+ // `pathParameters` and its `workspaceId` in `queryParameters`, while
223
+ // Outlook's `parentFolderId` — which the connector declares through the
224
+ // field's own `events.<OPERATION>` map rather than as a typed request
225
+ // parameter — lands in `eventParameters`. Emitting all of them as event
226
+ // parameters puts three of those four in a slot the runtime does not read.
227
+ const bucketOf = new Map();
228
+ const declaredOrder = [];
229
+ for (const f of (rc.v1def
230
+ ?.eventParameters?.fields ?? [])) {
231
+ if (typeof f?.name !== 'string')
232
+ continue;
233
+ declaredOrder.push(f.name);
234
+ if (typeof f?.bucket === 'string')
235
+ bucketOf.set(f.name, f.bucket);
236
+ }
237
+ const eventParameters = {};
238
+ const queryParameters = {};
239
+ const pathParameters = {};
240
+ // FLATTENED first: everything below keys on the DECLARED name, which is dotted
241
+ // (`channelData.channel.id`), while the generated `…Where` interface splits
242
+ // that into an object — so an author following the types writes the nested
243
+ // shape. Un-flattened it missed `bucketOf` (landing in the wrong slot) and
244
+ // then stringified into the match expression as
245
+ // `channelData == '[object Object]'`: a subscription that matches nothing,
246
+ // compiled clean and passed `check`.
247
+ for (const [k, v] of Object.entries(flattenWhere(where, new Set(declaredOrder)))) {
248
+ const slot = bucketOf.get(k);
249
+ (slot === 'query' ? queryParameters : slot === 'path' ? pathParameters : eventParameters)[k] = v;
250
+ }
251
+ // Only EVENT parameters fold into the match expression. They are the
252
+ // subscription's own scope — the mandatory clause the platform AND-joins —
253
+ // whereas a query or path parameter is part of the request that registers
254
+ // the subscription, not a property of the events it matches.
255
+ const scopeClauses = eventScopeClauses(eventParameters, nodeId);
256
+ // Each authored filter is read structurally (which operator, which value) and
257
+ // lowered with the designer's own grammar — see `event-filters.ts` for the
258
+ // measured table.
259
+ const conditions = eventFilterReads(filters, nodeId);
260
+ const filterClauses = conditions.map((c) => `(${eventFilterJmes(c)})`);
261
+ const filterExpression = [
262
+ ...scopeClauses,
263
+ ...(filterClauses.length > 0 ? [`(${filterClauses.join('&&')})`] : []),
264
+ ].join(' && ');
265
+ const instanceParameters = {
266
+ connectorKey: rc.connectorKey,
267
+ objectName,
268
+ activityType: op.activityType,
269
+ version: rc.version,
270
+ eventOperation: op.eventOperation,
271
+ eventMode: op.eventMode,
272
+ supportsStreaming: op.supportsStreaming ?? false,
273
+ };
274
+ const tree = conditions.length === 0 ? null : {
275
+ groupOperator: 0,
276
+ index: 0,
277
+ filters: conditions.map((c, i) => ({
278
+ ...eventFilterTreeLeaf(c),
279
+ // Deterministic, so recompiling the same flow is byte-identical (the
280
+ // designer mints random uuids here; a random one would churn every build).
281
+ uiId: stableId(`${nodeId}:${c.field}:${String(c.value)}:${i}`),
282
+ })),
283
+ groups: [],
284
+ uuId: stableId(`${nodeId}:filters`),
285
+ };
286
+ // The node's own Property binding: how the platform registers the
287
+ // subscription, and where `flow debug` reads its query parameters from.
288
+ // Pinned to designer-authored artifacts — the START form carries every
289
+ // subscription parameter as `{defaultValue, isExpression}` whatever its
290
+ // bucket (Jira `project,issuetype`; Asana `workspaceId,projectId`; Outlook
291
+ // `parentFolderId`), while the mid-flow WAIT form carries an EMPTY value and
292
+ // empty parameter lists: it joins a subscription the trigger registers,
293
+ // rather than registering one of its own.
294
+ const isStartForm = rc.nodeType.includes('.connector.trigger.');
295
+ const allParameters = {
296
+ ...eventParameters, ...queryParameters, ...pathParameters,
297
+ };
298
+ // Declared order, not the order the author happened to write `where` in, so
299
+ // the same subscription always emits the same bytes.
300
+ const orderedNames = [
301
+ ...declaredOrder.filter((n) => n in allParameters),
302
+ ...Object.keys(allParameters).filter((n) => !declaredOrder.includes(n)),
303
+ ];
304
+ const subscriptionValue = {};
305
+ if (isStartForm) {
306
+ for (const name of orderedNames) {
307
+ subscriptionValue[name] = { defaultValue: allParameters[name], isExpression: false };
308
+ }
309
+ }
310
+ const parameterList = isStartForm ? orderedNames.join(',') : '';
311
+ const propertyBinding = {
312
+ resource: 'Property',
313
+ key: op.eventOperation ?? '',
314
+ value: subscriptionValue,
315
+ activityDisplayName: 'connector-activity',
316
+ metadata: {
317
+ ActivityName: 'connector-activity',
318
+ BindingsVersion: '2.2',
319
+ ObjectName: objectName,
320
+ // The trigger resource is keyed by the CONNECTION the subscription is
321
+ // registered against, so two connections keep two trigger resources.
322
+ ...(conn ? { ParentResourceKey: `EventTrigger.${conn}` } : {}),
323
+ TriggerCreateParameters: parameterList,
324
+ DebugQueryParameters: parameterList,
325
+ },
326
+ };
327
+ return {
328
+ connector: rc.connectorKey,
329
+ ...(conn ? { connectionId: conn, connectionResourceId: conn } : {}),
330
+ ...(folder ? { connectionFolderKey: folder } : {}),
331
+ eventParameters,
332
+ // Emitted only when populated, mirroring the connector-activity slots:
333
+ // no archived artifact carries an empty one.
334
+ ...(Object.keys(queryParameters).length ? { queryParameters } : {}),
335
+ ...(Object.keys(pathParameters).length ? { pathParameters } : {}),
336
+ objectName,
337
+ eventType: op.eventOperation,
338
+ eventMode: op.eventMode,
339
+ filterExpression,
340
+ configuration: '=jsonString:' + JSON.stringify({
341
+ essentialConfiguration: {
342
+ instanceParameters,
343
+ objectName,
344
+ packageVersion: rc.version,
345
+ connectorVersion: null,
346
+ executionType: null,
347
+ httpMethod: null,
348
+ path: null,
349
+ filter: tree,
350
+ },
351
+ }),
352
+ ...(rc.activityTypeId ? { uiPathActivityTypeId: rc.activityTypeId } : {}),
353
+ bindings: [propertyBinding],
354
+ errorState: { issues: [] },
355
+ };
356
+ }
@@ -67,7 +67,9 @@ export declare class NodeTypeRegistry<Ctx extends ArtifactContexts> {
67
67
  * whose `typeNamePrefix` it starts with (and whose `typeNameMatches`, if any,
68
68
  * accepts it) — or undefined when no type owns it.
69
69
  * The bare prefix counts: it is what an empty `key` builds, and the old lift
70
- * read it as its factory, so it still does.
70
+ * read it as its factory, so it still does. Two prefix types that would both
71
+ * own a name (a nested prefix the outer narrowing failed to leave alone) are
72
+ * an SDK bug, and throw.
71
73
  */
72
74
  byTypeName(typeName: string): Registered<Ctx> | undefined;
73
75
  /** Every registered kind, sorted. */
@@ -5,6 +5,18 @@ import { CompileRefusal } from '../compile-refusal.js';
5
5
  function owns(p, typeName) {
6
6
  return typeName.startsWith(p.prefix) && (p.matches?.(typeName) ?? true);
7
7
  }
8
+ /**
9
+ * Whether one prefix sits strictly inside the other and the outer one is
10
+ * narrowed to leave it alone: the connector operations' `uipath.connector.`
11
+ * holds the event wait's `uipath.connector.event.`, and the operations'
12
+ * `typeNameMatches` rejects every event name. A name under the inner prefix is
13
+ * the probe; `byTypeName` still refuses a name two prefixes would both own.
14
+ */
15
+ function nests(a, b) {
16
+ const [outer, inner] = a.prefix.length < b.prefix.length ? [a, b] : [b, a];
17
+ return inner.prefix !== outer.prefix && inner.prefix.startsWith(outer.prefix)
18
+ && outer.matches !== undefined && !outer.matches(`${inner.prefix}probe.probe`);
19
+ }
8
20
  export class NodeTypeRegistry {
9
21
  artifact;
10
22
  #builtIn;
@@ -84,7 +96,8 @@ export class NodeTypeRegistry {
84
96
  if (type.typeName !== `${prefix}*`) {
85
97
  problems.push(`${where}: a type that owns the names under "${prefix}" is named "${prefix}*", not "${type.typeName}"`);
86
98
  }
87
- const overlap = index.byPrefix.find((p) => p.prefix.startsWith(prefix) || prefix.startsWith(p.prefix));
99
+ const self = { prefix, ...(type.typeNameMatches ? { matches: type.typeNameMatches.bind(type) } : {}) };
100
+ const overlap = index.byPrefix.find((p) => (p.prefix.startsWith(prefix) || prefix.startsWith(p.prefix)) && !nests(self, p));
88
101
  if (overlap)
89
102
  problems.push(`${where}: prefix "${prefix}" overlaps "${overlap.prefix}" (${placeOf(overlap.entry)})`);
90
103
  const shadowed = [...index.byTypeName.keys()].find((n) => n.startsWith(prefix) && (type.typeNameMatches?.(n) ?? true));
@@ -156,13 +169,19 @@ export class NodeTypeRegistry {
156
169
  * whose `typeNamePrefix` it starts with (and whose `typeNameMatches`, if any,
157
170
  * accepts it) — or undefined when no type owns it.
158
171
  * The bare prefix counts: it is what an empty `key` builds, and the old lift
159
- * read it as its factory, so it still does.
172
+ * read it as its factory, so it still does. Two prefix types that would both
173
+ * own a name (a nested prefix the outer narrowing failed to leave alone) are
174
+ * an SDK bug, and throw.
160
175
  */
161
176
  byTypeName(typeName) {
162
177
  const exact = this.#index.byTypeName.get(typeName);
163
178
  if (exact)
164
179
  return exact;
165
- return this.#index.byPrefix.find((p) => owns(p, typeName))?.entry;
180
+ const owners = this.#index.byPrefix.filter((p) => owns(p, typeName));
181
+ if (owners.length > 1) {
182
+ throw new Error(`internal SDK error: the node type name "${typeName}" is owned by ${owners.map((o) => placeOf(o.entry)).join(' and ')}; exactly one may.`);
183
+ }
184
+ return owners[0]?.entry;
166
185
  }
167
186
  /** Every registered kind, sorted. */
168
187
  kinds() {
package/dist/decompile.js CHANGED
@@ -912,13 +912,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
912
912
  return lifted;
913
913
  switch (node.type) {
914
914
  default:
915
- // A mid-flow connector EVENT wait (`uipath.connector.event.<key>.<event>`
916
- // placed as a step rather than as the start node).
917
- if (node.type.startsWith('uipath.connector.event.')) {
918
- const sub = emitEventSubscription(node, imp, inputNames);
919
- if (sub !== undefined)
920
- return `${imp.use('waitForEvent')}(${sub})`;
921
- }
922
915
  // Every family this SDK AUTHORS is recovered above — a guard test asserts
923
916
  // it (`flow-decompile-families.test.ts`). Reaching here means a node type
924
917
  // the SDK cannot author: a hand-written file, a newer platform family, or
@@ -16,6 +16,7 @@ import type { DefinitionReference, NodeContribution } from './core/node-classes.
16
16
  import type { CheckOpts } from './check.js';
17
17
  import type { SerializeOpts } from './serialize.js';
18
18
  import type { DeclaredExit, NoExits } from './core/step-ports.js';
19
+ import type { EventEmitEnv } from './core/event-subscription.js';
19
20
  /**
20
21
  * What a Flow node type's `serialize` returns: the node's type-owned fields.
21
22
  *
@@ -240,6 +241,12 @@ export interface FlowSerializeContext {
240
241
  emitted(typeName: string, version: string): unknown;
241
242
  /** The connector library serialize was given, for a type that resolves a connector operation. */
242
243
  readonly library: SerializeOpts['library'];
244
+ /**
245
+ * What an event subscription's shared emission (`core/event-subscription.ts`)
246
+ * needs: the library, the bindings, and how a label resolves, with compile's
247
+ * refusals and warnings.
248
+ */
249
+ readonly events: EventEmitEnv;
243
250
  /**
244
251
  * Render a value and every `Expr` nested in its objects and arrays, as a
245
252
  * connector's inputs are. A `lookup()` token is refused: it belongs only where a
@@ -408,6 +415,14 @@ export interface FlowCheckContext {
408
415
  key: string;
409
416
  action?: string;
410
417
  }): void;
418
+ /**
419
+ * Run the rules every event subscription shares, against the library facts
420
+ * check was given (`options`): the subscription's own shape and its
421
+ * resolution against the library. `where` is how the site is named
422
+ * (`waitForEvent "<step>"`). What compile refuses is the serializer's,
423
+ * reported from its run; each rule is reported at its own severity.
424
+ */
425
+ eventSubscription(subscription: unknown, where: string): void;
411
426
  /**
412
427
  * Run the rules every Integration Service operation shares that compile does
413
428
  * not: its lookups and its schema readiness, against the library facts check
@@ -564,6 +579,16 @@ export interface FlowNodeType<Spec> extends NodeType<Spec, FlowNodeContribution,
564
579
  * `errorPort`, so the two rungs give one answer; undefined when it cannot.
565
580
  */
566
581
  checkDefinition?(spec: Spec, options: CheckOpts): unknown;
582
+ /**
583
+ * The node a spec builds, as far as the spec itself names it, for a per-tenant
584
+ * prefix type: its concrete type name, and the version the spec pins. A refused
585
+ * step's stand-in names its node by it, before any definition is in hand (an
586
+ * event wait refused for want of a library still names its event).
587
+ */
588
+ namedNode?(spec: Spec): {
589
+ typeName: string;
590
+ version?: string;
591
+ } | undefined;
567
592
  }
568
593
  /** A Flow node type with several versions. */
569
594
  export type FlowVersionedNodeType<Spec> = VersionedNodeType<Spec, FlowNodeContribution, FlowContexts, FlowNodeType<Spec>>;