@uipath/maestro-builder-sdk 6.70.8 → 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 +11 -11
- package/dist/core/connector-checks.js +1 -1
- package/dist/core/event-subscription.d.ts +113 -0
- package/dist/core/event-subscription.js +356 -0
- package/dist/core/nodes/registry.d.ts +3 -1
- package/dist/core/nodes/registry.js +22 -3
- package/dist/decompile.js +0 -7
- package/dist/flow-node-type.d.ts +25 -0
- package/dist/nodes/connector/flow/event-wait.d.ts +33 -0
- package/dist/nodes/connector/flow/event-wait.js +162 -0
- package/dist/nodes/connector/flow/family.d.ts +1 -1
- package/dist/nodes/connector/flow/family.js +3 -2
- package/dist/serialize.js +20 -391
- package/package.json +2 -2
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) => {
|
|
@@ -260,7 +260,7 @@ export function checkConnectorSchema(where, spec, opts, dialect, report) {
|
|
|
260
260
|
message: `${where}: object "${spec.object}" has no locally materialized schema — `
|
|
261
261
|
+ `this operation resolves its object against the connection, so its fields cannot be `
|
|
262
262
|
+ `validated here and compile will emit it unverified. Prepare it once:\n`
|
|
263
|
-
+ ` ${
|
|
263
|
+
+ ` ${prepareCommand(spec.key, spec.action, ...(spec.object ? ['--object', spec.object] : []))}\n`
|
|
264
264
|
+ `The name is matched against the connected catalog (exact, then case-insensitive on `
|
|
265
265
|
+ `name and display name); several matches fail with the candidates to choose from, and `
|
|
266
266
|
+ `the prepared descriptor is the import to switch to.`,
|
|
@@ -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
|
|
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
|
-
|
|
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
|
package/dist/flow-node-type.d.ts
CHANGED
|
@@ -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>>;
|