@uipath/maestro-builder-sdk 6.14.5 → 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 +71 -71
- package/dist/api-members.md +324 -323
- 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 +33 -709
- 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/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 +27 -420
- 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,321 @@
|
|
|
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 { isLookupToken, resolvedValue, unresolvedLookupMessage, } from './lookups.js';
|
|
20
|
+
import { prepareCommand } from './cli-spelling.js';
|
|
21
|
+
import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem } from './event-filters.js';
|
|
22
|
+
import { EVENT_BYOA_UNVERIFIED, byoaUnverified } from './byoa.js';
|
|
23
|
+
/**
|
|
24
|
+
* Ask the library what event parameters a subscription's operation declares.
|
|
25
|
+
* Returns `undefined` when the subscription is too broken to look up (its own
|
|
26
|
+
* diagnostics say so) or when no resolver was supplied — both mean "unknown", and
|
|
27
|
+
* unknown fails open.
|
|
28
|
+
*/
|
|
29
|
+
export function eventParamsFor(subscription, opts) {
|
|
30
|
+
if (!opts.eventParams)
|
|
31
|
+
return undefined;
|
|
32
|
+
const sub = subscription;
|
|
33
|
+
if (typeof sub?.connector !== 'string' || typeof sub?.event !== 'string')
|
|
34
|
+
return undefined;
|
|
35
|
+
if (sub.connector === '' || sub.event === '')
|
|
36
|
+
return undefined;
|
|
37
|
+
// A GENERIC event's schema is per OBJECT once the library carries baked
|
|
38
|
+
// per-object variants, so the object is part of the subscription's identity
|
|
39
|
+
// here. Without it the lookup asks for a node type that resolves to no
|
|
40
|
+
// single variant and answers "unknown", which reads as EVENT_UNPREPARED on
|
|
41
|
+
// an event the library actually knows.
|
|
42
|
+
const object = typeof sub.object === 'string' && sub.object !== ''
|
|
43
|
+
? sub.object : undefined;
|
|
44
|
+
return opts.eventParams(sub.connector, sub.event, object);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The event-side resolution rules — the trigger twin of `checkConnectorLookups`,
|
|
48
|
+
* plus three rules with no activity counterpart:
|
|
49
|
+
*
|
|
50
|
+
* - `EVENT_UNKNOWN` — an ERROR: a library is loaded and it cannot resolve the
|
|
51
|
+
* subscription, so `compile` will refuse it. The message is the resolver's
|
|
52
|
+
* own (`eventResolveError`), the same text compile throws. Checked first; when
|
|
53
|
+
* it fires, EVENT_UNPREPARED and the filter rules below are skipped for this
|
|
54
|
+
* subscription, since there is no entry for them to read.
|
|
55
|
+
* - `EVENT_UNPREPARED` — the event RESOLVES but the library knows nothing
|
|
56
|
+
* about its parameters (or no `eventResolveError` resolver was supplied):
|
|
57
|
+
* the fallback case for a connector the shipped library was not generated
|
|
58
|
+
* against. A warning, because compile still succeeds. Prints the one
|
|
59
|
+
* command that makes the event checkable.
|
|
60
|
+
* - `EVENT_BYOA_UNVERIFIED` — an ERROR: the event only fires on a BYOA
|
|
61
|
+
* (bring-your-own-app) connection, and the bound connection id has no record
|
|
62
|
+
* of `prepare` having checked it (`core/byoa.ts` has the rule and when it
|
|
63
|
+
* stays silent). A standard connection deploys and the trigger never fires;
|
|
64
|
+
* the usual way in is binding the connection by preparing one of the
|
|
65
|
+
* connector's ACTIONS, which checks nothing about BYOA. `compile` throws the
|
|
66
|
+
* same text.
|
|
67
|
+
* - `EVENT_FILTER_UNKNOWN_FIELD` — a filter leaf naming a field outside the
|
|
68
|
+
* prepared vocabulary is an ERROR, not a warning, because the platform's own
|
|
69
|
+
* configure DROPS an unmatched leaf and reports Success: the deployed
|
|
70
|
+
* trigger then fires on events the filter should exclude, silently. Matching
|
|
71
|
+
* is case-sensitive, so the near-miss hint checks case first. When the
|
|
72
|
+
* vocabulary is unknown or known-empty this stays quiet — fail open.
|
|
73
|
+
*
|
|
74
|
+
* Unresolved `where` lookups follow the activity rule exactly: a WARNING here
|
|
75
|
+
* (compile repeats it as the hard failure), with the recorded answer looked up
|
|
76
|
+
* under the event-form node type — the form `prepare` stores.
|
|
77
|
+
*/
|
|
78
|
+
export function checkEventResolution(subscription, where, kind, opts, error, warn, eventParams) {
|
|
79
|
+
const sub = subscription;
|
|
80
|
+
if (!sub || typeof sub.connector !== 'string' || typeof sub.event !== 'string'
|
|
81
|
+
|| sub.connector === '' || sub.event === '')
|
|
82
|
+
return;
|
|
83
|
+
// Captured because the guard above narrows `sub.connector` / `sub.event` only
|
|
84
|
+
// in THIS scope: a property narrowing does not survive into the `filters`
|
|
85
|
+
// callback below, where they are `unknown` again. The old messages hid that
|
|
86
|
+
// by interpolating them into template literals, which accept anything.
|
|
87
|
+
const connectorKey = sub.connector;
|
|
88
|
+
const eventName = sub.event;
|
|
89
|
+
// Exactly what `serialize` passes to `resolveEvent` — `sub.version` and
|
|
90
|
+
// `sub.object` as the author wrote them — so the verdict is compile's.
|
|
91
|
+
const unresolved = opts.eventResolveError?.(kind, {
|
|
92
|
+
connector: connectorKey,
|
|
93
|
+
event: eventName,
|
|
94
|
+
...(typeof sub.version === 'string' ? { version: sub.version } : {}),
|
|
95
|
+
...(typeof sub.object === 'string' ? { object: sub.object } : {}),
|
|
96
|
+
});
|
|
97
|
+
if (unresolved !== undefined) {
|
|
98
|
+
error('EVENT_UNKNOWN', `${where}: ${unresolved}`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
|
|
102
|
+
const byoa = opts.eventFacts && byoaUnverified(where, { connector: connectorKey, event: eventName, ...(object ? { object } : {}), connection: sub.connection }, opts.eventFacts(connectorKey, eventName, object), opts.byoa, opts.bindings);
|
|
103
|
+
if (byoa)
|
|
104
|
+
error(EVENT_BYOA_UNVERIFIED, byoa);
|
|
105
|
+
if (eventParams?.objectUnprepared && object) {
|
|
106
|
+
// The generic template answers for every object, so resolution SUCCEEDS
|
|
107
|
+
// and nothing downstream notices that the object named here has no
|
|
108
|
+
// schema: no unprepared warning, and a filter vocabulary that fails open.
|
|
109
|
+
// That is the silent case this whole surface exists to remove, one level
|
|
110
|
+
// up from the activity it was first found on.
|
|
111
|
+
warn('OBJECT_UNPREPARED', `${where}: object \`${object}\` has no prepared schema for `
|
|
112
|
+
+ `\`${sub.connector}\` \`${sub.event}\`, and this connector's objects come from `
|
|
113
|
+
+ `the connection, so the library cannot carry one. Materialize it:\n`
|
|
114
|
+
+ ` ${prepareCommand(connectorKey, eventName)} --object ${object}\n`
|
|
115
|
+
+ `Until then its event parameters and filterable fields are unknown and `
|
|
116
|
+
+ `nothing here can check them.`);
|
|
117
|
+
}
|
|
118
|
+
if (opts.eventParams && eventParams === undefined) {
|
|
119
|
+
warn('EVENT_UNPREPARED', `${where}: the library knows nothing about connector event \`${sub.connector}\` \`${sub.event}\` — `
|
|
120
|
+
+ `its parameters and filterable fields cannot be verified offline. Prepare it against a live connection:\n`
|
|
121
|
+
+ ` ${prepareCommand(connectorKey, eventName)}\n`
|
|
122
|
+
+ `and re-run check: the prepared overlay makes this event checkable.`);
|
|
123
|
+
}
|
|
124
|
+
const resolutions = opts.resolutions ?? {};
|
|
125
|
+
const scope = sub.where && typeof sub.where === 'object'
|
|
126
|
+
? Object.entries(sub.where) : [];
|
|
127
|
+
for (const [key, value] of scope) {
|
|
128
|
+
if (!isLookupToken(value))
|
|
129
|
+
continue;
|
|
130
|
+
if (resolvedValue(value, resolutions) !== undefined)
|
|
131
|
+
continue;
|
|
132
|
+
warn('LOOKUP_UNRESOLVED', `${where}: \`where.${key}\` ` + unresolvedLookupMessage(value)
|
|
133
|
+
+ `\nCompile will fail until this is recorded.`);
|
|
134
|
+
}
|
|
135
|
+
const names = opts.eventFilterFields?.(sub.connector, sub.event, object);
|
|
136
|
+
if (names && names.length > 0 && Array.isArray(sub.filters)) {
|
|
137
|
+
const known = new Set(names);
|
|
138
|
+
sub.filters.forEach((leaf, i) => {
|
|
139
|
+
const { read } = readEventFilter(leaf);
|
|
140
|
+
if (!read || known.has(read.field))
|
|
141
|
+
return;
|
|
142
|
+
const caseSlip = names.find((n) => n.toLowerCase() === read.field.toLowerCase());
|
|
143
|
+
// A case slip is a mistake whatever the vocabulary's completeness — the
|
|
144
|
+
// platform matches case-sensitively, so `Subject` is dropped even where
|
|
145
|
+
// `subject` exists. An unrecognized name is only certainly wrong when
|
|
146
|
+
// the vocabulary is COMPLETE; where required selectors decide the field
|
|
147
|
+
// set and nothing can supply them, refusing a legal field would be the
|
|
148
|
+
// worse error, so it warns.
|
|
149
|
+
const selectors = eventParams?.selectors ?? [];
|
|
150
|
+
// A hybrid connector's custom filter fields are left for `prepare` BY
|
|
151
|
+
// DESIGN, so the baked list is a strict subset of the tenant's and a
|
|
152
|
+
// name outside it may be perfectly real. Erroring there tells the author
|
|
153
|
+
// the platform will drop a field it will actually honour, and the only
|
|
154
|
+
// way out is deleting a correct filter — "not in my snapshot" mistaken
|
|
155
|
+
// for "wrong", which is the trap CUSTOM_FIELDS_UNPREPARED was narrowed
|
|
156
|
+
// around for activities. Same shape here, and the same remedy: name the
|
|
157
|
+
// prepare that closes the gap.
|
|
158
|
+
if (eventParams?.customFieldsUnprepared && !caseSlip) {
|
|
159
|
+
warn('CUSTOM_FIELDS_UNPREPARED', `${where}: filters[${i}] names \`${read.field}\`, which is not in the `
|
|
160
|
+
+ `tenant-agnostic vocabulary for \`${sub.connector}\` \`${sub.event}\`. This `
|
|
161
|
+
+ `connector discovers CUSTOM FIELDS per connection, so the baked list is a `
|
|
162
|
+
+ `subset by design and this name may be a real field of yours. Prepare the `
|
|
163
|
+
+ `event against your connection to check it for real:\n`
|
|
164
|
+
+ ` ${prepareCommand(connectorKey, eventName)}`
|
|
165
|
+
+ `${object ? ` --object ${object}` : ''}\n`
|
|
166
|
+
+ `Baked fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Same shape as the branch above, for the gap preparing cannot close.
|
|
170
|
+
// `customFieldsUnprepared` says "run prepare and this resolves";
|
|
171
|
+
// `filtersPartial` says "no call reaches these fields at all", so the
|
|
172
|
+
// remedy is the designer, not another command.
|
|
173
|
+
//
|
|
174
|
+
// Yields to the selector branch below, which reaches the same WARNING by
|
|
175
|
+
// a more specific route: where `project` / `issuetype` select the schema
|
|
176
|
+
// it can name them, and "these two values decide the field set" is more
|
|
177
|
+
// use to an author than "this connector reads its schema from the
|
|
178
|
+
// connection". Both are true of Jira; the named one is the better message.
|
|
179
|
+
if (eventParams?.filtersPartial && !caseSlip && selectors.length === 0) {
|
|
180
|
+
warn('EVENT_FILTER_UNKNOWN_FIELD', `${where}: filters[${i}] names \`${read.field}\`, which is not in the known `
|
|
181
|
+
+ `filter vocabulary for \`${sub.connector}\` \`${sub.event}\`. This connector `
|
|
182
|
+
+ `discovers custom fields per connection, and the trigger metadata call takes no `
|
|
183
|
+
+ `parent values (\`uip is triggers describe\` has no \`-f\`), so the list below is a `
|
|
184
|
+
+ `subset NO prepare can complete — this name may be a real field of yours. `
|
|
185
|
+
+ `Verify it against the trigger in the designer before deploying.\n`
|
|
186
|
+
+ `Known fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const baseline = selectors.length > 0 && !caseSlip;
|
|
190
|
+
(baseline ? warn : error)('EVENT_FILTER_UNKNOWN_FIELD', `${where}: filters[${i}] names \`${read.field}\`, which is not a filterable field of `
|
|
191
|
+
+ `\`${sub.connector}\` \`${sub.event}\`. The platform DROPS an unmatched leaf and the deployed `
|
|
192
|
+
+ `trigger fires on events this filter should exclude. `
|
|
193
|
+
+ (caseSlip
|
|
194
|
+
? `Did you mean \`${caseSlip}\`? Matching is case-sensitive.`
|
|
195
|
+
: `Filterable fields: ${names.slice(0, 12).join(', ')}${names.length > 12 ? ', …' : ''}.`)
|
|
196
|
+
+ (baseline
|
|
197
|
+
? ` This is a WARNING rather than an error because \`${selectors.join('`, `')}\` `
|
|
198
|
+
+ `select this event's schema and no discovery call accepts their values, so the `
|
|
199
|
+
+ `list above is a baseline the real vocabulary extends. Verify the name against `
|
|
200
|
+
+ `the trigger in the designer before deploying.`
|
|
201
|
+
: ''));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export function checkEventSubscription(subscription, where, at, warn, eventParams) {
|
|
206
|
+
const sub = subscription;
|
|
207
|
+
if (!sub || typeof sub !== 'object') {
|
|
208
|
+
at('EVENT_NO_SUBSCRIPTION', `${where} has no event subscription. Pass one: `
|
|
209
|
+
+ `{ connector: 'uipath-microsoft-outlook365', event: 'email-received', where: { parentFolderId: '<id>' } }.`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (typeof sub.connector !== 'string' || sub.connector === '') {
|
|
213
|
+
at('EVENT_NO_CONNECTOR', `${where} has no \`connector\` key (e.g. 'uipath-microsoft-outlook365').`);
|
|
214
|
+
}
|
|
215
|
+
if (typeof sub.event !== 'string' || sub.event === '') {
|
|
216
|
+
at('EVENT_NO_EVENT', `${where} has no \`event\` name (e.g. 'email-received').`);
|
|
217
|
+
}
|
|
218
|
+
const scope = sub.where;
|
|
219
|
+
const scopeEntries = scope && typeof scope === 'object'
|
|
220
|
+
? Object.entries(scope)
|
|
221
|
+
.filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
|
|
222
|
+
: [];
|
|
223
|
+
const scopeKeys = scopeEntries.map(([k]) => k);
|
|
224
|
+
// The object an author put in `where` by mistake — `objectName` is what the
|
|
225
|
+
// platform's own `node configure --detail` calls it, so it is the natural slip.
|
|
226
|
+
const scopeObject = scopeEntries.find(([k]) => /^(objectName|entityName|object)$/i.test(k))?.[1];
|
|
227
|
+
const object = typeof sub.object === 'string' && sub.object !== '' ? sub.object : undefined;
|
|
228
|
+
const say = warn ?? at;
|
|
229
|
+
// The OBJECT half of the contract. A GENERIC event (`record-created` on Data
|
|
230
|
+
// Fabric, Salesforce, ServiceNow, Jira, …) fires for ONE object of the connection
|
|
231
|
+
// and its library entry names none — so `object` is REQUIRED, and its absence is
|
|
232
|
+
// an error: the node would carry `objectName: ""`, validate, and be refused by
|
|
233
|
+
// the platform's dispatcher (or watch nothing). A CURATED event has its object
|
|
234
|
+
// built in, so an `object` there names a choice that does not exist.
|
|
235
|
+
if (eventParams?.generic) {
|
|
236
|
+
if (object === undefined) {
|
|
237
|
+
at('EVENT_GENERIC_NO_OBJECT', `${where} subscribes to a GENERIC event: \`${String(sub.connector)}\` \`${String(sub.event)}\` fires for ONE `
|
|
238
|
+
+ `object of the connection (an entity, a table, a custom object) and the library carries no default. `
|
|
239
|
+
+ `Name it with \`object\`.`
|
|
240
|
+
+ (scopeObject !== undefined
|
|
241
|
+
? ` \`where: { ${scopeKeys.join(', ')} }\` is not it — \`where\` holds event PARAMETERS, and this `
|
|
242
|
+
+ `operation takes none; the value you put there is the object.`
|
|
243
|
+
: ` \`uip is triggers objects ${String(sub.connector)} <EVENT> --connection-id <id>\` lists the objects `
|
|
244
|
+
+ `the bound connection exposes.`), `object: '${scopeObject !== undefined ? String(scopeObject) : '<Object>'}'`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (object !== undefined && eventParams?.known) {
|
|
248
|
+
at('EVENT_OBJECT_NOT_GENERIC', `${where} passes \`object: '${object}'\`, but \`${String(sub.connector)}\` \`${String(sub.event)}\` is a `
|
|
249
|
+
+ `CURATED event whose object is built in`
|
|
250
|
+
+ (eventParams.object ? ` (\`${eventParams.object}\`)` : '')
|
|
251
|
+
+ `. \`object\` names the object of a GENERIC event only (\`record-created\` and its kin); drop it here.`, undefined);
|
|
252
|
+
}
|
|
253
|
+
// The operation declares no event parameters: an empty `where` is the ONLY
|
|
254
|
+
// correct authoring, and a non-empty one names fields the operation has not got.
|
|
255
|
+
if (eventParams?.known && eventParams.names.length === 0) {
|
|
256
|
+
if (scopeKeys.length > 0) {
|
|
257
|
+
say('EVENT_SCOPE_NOT_TAKEN', `${where} passes \`where: { ${scopeKeys.join(', ')} }\`, but this connector operation takes NO event `
|
|
258
|
+
+ `parameters — its library entry declares \`inputSchema.fields: []\`, and \`uip is triggers describe\` `
|
|
259
|
+
+ `reports \`EventParameters: []\`. The subscription is scoped by the CONNECTION alone, so these keys `
|
|
260
|
+
+ `are carried into \`detail.eventParameters\` and matched against nothing. `
|
|
261
|
+
+ (eventParams.generic && scopeObject !== undefined
|
|
262
|
+
? `The object this GENERIC event watches goes in \`object\`, not \`where\`.`
|
|
263
|
+
: `Filter on the PAYLOAD instead if you meant to narrow it.`), eventParams.generic && scopeObject !== undefined
|
|
264
|
+
? `object: '${String(scopeObject)}'`
|
|
265
|
+
: `where: {}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (scopeKeys.length === 0 && !eventParams?.generic) {
|
|
269
|
+
// Whether omitting the scope is a mistake is not knowable here — the platform
|
|
270
|
+
// accepts an unscoped subscription (it listens to everything the connection can
|
|
271
|
+
// see) and 12 deployed Outlook flows that do exactly that scored 1.0. Name the
|
|
272
|
+
// fields the operation actually has and let the author decide.
|
|
273
|
+
const fields = eventParams?.known && eventParams.names.length > 0
|
|
274
|
+
? `Its event parameters are: ${eventParams.names.map((n) => `\`${n}\``).join(', ')}`
|
|
275
|
+
: "\`where\` carries the connector's event parameters — for Outlook email-received that is the mail folder";
|
|
276
|
+
say('EVENT_NO_SCOPE', `${where} has no \`where\`, so its subscription is UNSCOPED: it listens to everything the bound `
|
|
277
|
+
+ `connection can see, not the one thing you named. That deploys and runs — it is broader than you `
|
|
278
|
+
+ `probably meant, which is why this is a warning and not an error. ${fields}. `
|
|
279
|
+
+ `The names and their ids come from the bound connection: `
|
|
280
|
+
+ `\`uip is triggers describe <connector> <EVENT> <object> --connection-id <id>\` lists them, and `
|
|
281
|
+
+ `\`uip is resources run list <connector> MailFolder --connection-id <id>\` resolves a folder id. `
|
|
282
|
+
+ `If the operation takes no event parameters at all — HTTP Webhook is one — an empty \`where\` `
|
|
283
|
+
+ `is correct, and with a library on hand this warning does not fire.`, eventParams?.known && eventParams.names.length > 0
|
|
284
|
+
? `where: { ${eventParams.names[0]}: '<id>' }`
|
|
285
|
+
: `where: { parentFolderId: '<mail folder id>' }`);
|
|
286
|
+
}
|
|
287
|
+
const filters = sub.filters;
|
|
288
|
+
if (filters !== undefined) {
|
|
289
|
+
if (!Array.isArray(filters)) {
|
|
290
|
+
at('EVENT_FILTERS_NOT_ARRAY', `${where}'s \`filters\` must be an ARRAY of { field, <operator>: value } — filters are positional and `
|
|
291
|
+
+ `carry author-supplied field names, so an object map cannot express them.`, `filters: [{ field: 'subject', contains: 'Report' }]`);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const operators = Object.keys(EVENT_FILTER_OPERATORS).join(', ');
|
|
295
|
+
filters.forEach((f, i) => {
|
|
296
|
+
const leaf = f;
|
|
297
|
+
if (!leaf || typeof leaf.field !== 'string' || leaf.field === '') {
|
|
298
|
+
at('EVENT_FILTER_NO_FIELD', `${where}'s filters[${i}] has no \`field\` — name a field on the event payload, e.g. 'subject'.`);
|
|
299
|
+
}
|
|
300
|
+
const { keys, read } = readEventFilter(leaf);
|
|
301
|
+
if (keys.length === 0) {
|
|
302
|
+
at('EVENT_FILTER_NO_VALUE', `${where}'s filters[${i}] has no operator. Write exactly one of ${operators} with its value — `
|
|
303
|
+
+ `\`contains\` is a case-sensitive substring test, the same one Integration Service applies `
|
|
304
|
+
+ `server-side; \`lessThan\`/\`greaterThan\` order a number or an ISO date.`, `{ field: '${typeof leaf?.field === 'string' ? leaf.field : 'subject'}', contains: '<text>' }`);
|
|
305
|
+
}
|
|
306
|
+
else if (keys.length > 1) {
|
|
307
|
+
at('EVENT_FILTER_MANY_OPERATORS', `${where}'s filters[${i}] carries ${keys.length} operators (${keys.join(', ')}); a condition has exactly `
|
|
308
|
+
+ `one. Write two conditions instead — they are ANDed.`);
|
|
309
|
+
}
|
|
310
|
+
else if (!read) {
|
|
311
|
+
at('EVENT_FILTER_NO_VALUE', `${where}'s filters[${i}] gives \`${keys[0]}\` a value that is not a string, number or boolean.`);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
const problem = eventFilterProblem(read);
|
|
315
|
+
if (problem)
|
|
316
|
+
at('EVENT_FILTER_BAD_VALUE', `${where}'s filters[${i}] on \`${read.field}\`: ${problem}`);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
package/dist/decompile.js
CHANGED
|
@@ -38,7 +38,7 @@ import { DEFAULT_TRIGGER_ID } from './core/expr.js';
|
|
|
38
38
|
import { parseIxpProjectType } from './core/actions.js';
|
|
39
39
|
import { readFlowGlobals } from './flow-variables.js';
|
|
40
40
|
import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
|
|
41
|
-
import { eventFilterFromTreeLeaf } from './event-filters.js';
|
|
41
|
+
import { eventFilterFromTreeLeaf } from './core/event-filters.js';
|
|
42
42
|
/**
|
|
43
43
|
* The trigger id of the flow currently being decompiled, so a reference through a
|
|
44
44
|
* RENAMED trigger is still recognized as `input('x')` rather than falling back to
|
|
@@ -10,7 +10,13 @@ export declare function nestInputs(pairs: ReadonlyArray<readonly [unknown, strin
|
|
|
10
10
|
* helper; an enum shows its FIRST DECLARED CHOICE, typed to the declaration
|
|
11
11
|
* where it can be; anything else shows `input('<name>')`.
|
|
12
12
|
*/
|
|
13
|
-
export declare function exampleValue(f: Dict, symbol: string | undefined, invariant: ReadonlySet<string>, connectorKey?: string, action?: string
|
|
13
|
+
export declare function exampleValue(f: Dict, symbol: string | undefined, invariant: ReadonlySet<string>, connectorKey?: string, action?: string,
|
|
14
|
+
/**
|
|
15
|
+
* BPMN spelling: `.connector()` there takes the key and action as STRINGS —
|
|
16
|
+
* there is no generated descriptor to import — and a process reads a variable
|
|
17
|
+
* as the `=`-expression `=vars.<name>` rather than through `input()`.
|
|
18
|
+
*/
|
|
19
|
+
bpmnForm?: boolean): string;
|
|
14
20
|
/** A compilable `connector(...)` call for this operation; events go to {@link eventExampleLines}. */
|
|
15
21
|
export declare function exampleLines(j: Dict, fields: readonly Dict[], symbol: string | undefined, invariant: ReadonlySet<string>, v1def: Dict | undefined): string[];
|
|
16
22
|
/**
|
|
@@ -93,7 +93,13 @@ export function nestInputs(pairs) {
|
|
|
93
93
|
* helper; an enum shows its FIRST DECLARED CHOICE, typed to the declaration
|
|
94
94
|
* where it can be; anything else shows `input('<name>')`.
|
|
95
95
|
*/
|
|
96
|
-
export function exampleValue(f, symbol, invariant, connectorKey = "", action = ""
|
|
96
|
+
export function exampleValue(f, symbol, invariant, connectorKey = "", action = "",
|
|
97
|
+
/**
|
|
98
|
+
* BPMN spelling: `.connector()` there takes the key and action as STRINGS —
|
|
99
|
+
* there is no generated descriptor to import — and a process reads a variable
|
|
100
|
+
* as the `=`-expression `=vars.<name>` rather than through `input()`.
|
|
101
|
+
*/
|
|
102
|
+
bpmnForm = false) {
|
|
97
103
|
const [name] = authoringField(f);
|
|
98
104
|
// The lookup table is keyed by the WIRE name (`addLabelIds[*]`), which is
|
|
99
105
|
// what `--resolve` and `buildConnectorInputs` both match on.
|
|
@@ -104,7 +110,7 @@ export function exampleValue(f, symbol, invariant, connectorKey = "", action = "
|
|
|
104
110
|
const names = strList(reference.lookupNames);
|
|
105
111
|
const aliases = byAliases(names);
|
|
106
112
|
const first = names.length ? names[0] : "id";
|
|
107
|
-
if (symbol) {
|
|
113
|
+
if (symbol && !bpmnForm) {
|
|
108
114
|
if (aliases.size)
|
|
109
115
|
return `lookup(${symbol}, '${wire}').${firstValue(aliases)}('…')`;
|
|
110
116
|
return `lookup(${symbol}, '${wire}').by('${first}', '…')`;
|
|
@@ -131,7 +137,64 @@ export function exampleValue(f, symbol, invariant, connectorKey = "", action = "
|
|
|
131
137
|
return pyJsonString(choice);
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
|
-
return `input('${leafName(name)}')`;
|
|
140
|
+
return bpmnForm ? `'=vars.${leafName(name)}'` : `input('${leafName(name)}')`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* `### BPMN` — the same operation as a `.connector()` call on the BPMN builder.
|
|
144
|
+
*
|
|
145
|
+
* The Flow block above cannot be pasted into a `.bpmn.ts`: BPMN leads with the
|
|
146
|
+
* ELEMENT ID, never takes a generated descriptor (so `lookup()` is always the
|
|
147
|
+
* string form), and reads a variable as `=vars.x`. An agent handed this page by
|
|
148
|
+
* `registry search --limit 5` and told to author BPMN was being handed Flow.
|
|
149
|
+
*/
|
|
150
|
+
function bpmnExampleLines(required, invariant, connectorKey, action, step, runtime, operation) {
|
|
151
|
+
const pairs = required.map((f) => {
|
|
152
|
+
const [name, , wireName] = authoringField(f);
|
|
153
|
+
return [wireName ? f.name : name, exampleValue(f, undefined, invariant, connectorKey, action, true)];
|
|
154
|
+
});
|
|
155
|
+
const out = ["### BPMN", "", "```ts"];
|
|
156
|
+
out.push(`import { bpmn } from '${PACKAGE_NAME}/bpmn';`);
|
|
157
|
+
if (pairs.some(([, v]) => v.includes("lookup(")))
|
|
158
|
+
out.push(`import { lookup } from '${PACKAGE_NAME}';`);
|
|
159
|
+
out.push("");
|
|
160
|
+
out.push(`.connector('${step}', '${connectorKey}', '${action}', {`);
|
|
161
|
+
if (pairs.length)
|
|
162
|
+
out.push(...nestInputs(pairs));
|
|
163
|
+
const opts = [];
|
|
164
|
+
if (pyTruthy(runtime.requiresConnection))
|
|
165
|
+
opts.push(`connection: '${afterLast(connectorKey, "-")}'`);
|
|
166
|
+
if (pyTruthy(runtime.requiresFolderKey))
|
|
167
|
+
opts.push("folder: 'shared'");
|
|
168
|
+
if (operation.activityType === "Generic" || operation.activityType === "GenericExpanded") {
|
|
169
|
+
opts.push(`object: '${pyStr(pyOr(operation.objectName, "<object>"))}'`);
|
|
170
|
+
}
|
|
171
|
+
out.push("}" + (opts.length ? `, { ${opts.join(", ")} }` : "") + ")");
|
|
172
|
+
out.push("```");
|
|
173
|
+
out.push("");
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* `### BPMN` for a connector EVENT: `.eventTrigger()` starts a process,
|
|
178
|
+
* `.waitForEvent()` pauses one. The subscription scope is `filter` and its
|
|
179
|
+
* parameters are FLAT there, where Flow's `where` nests them.
|
|
180
|
+
*/
|
|
181
|
+
function bpmnEventExampleLines(params, generic, start, connectorKey, action, step) {
|
|
182
|
+
const method = start ? "eventTrigger" : "waitForEvent";
|
|
183
|
+
const opts = [`connection: '${afterLast(connectorKey, "-")}'`, "folder: 'shared'"];
|
|
184
|
+
if (generic)
|
|
185
|
+
opts.push("object: '<Object>'");
|
|
186
|
+
if (params.length)
|
|
187
|
+
opts.push(`filter: { ${params.map((p) => `${IDENT.test(p) ? p : pyJsonString(p)}: '<${p}>'`).join(", ")} }`);
|
|
188
|
+
return [
|
|
189
|
+
"### BPMN",
|
|
190
|
+
"",
|
|
191
|
+
"```ts",
|
|
192
|
+
`import { bpmn } from '${PACKAGE_NAME}/bpmn';`,
|
|
193
|
+
"",
|
|
194
|
+
`.${method}('${step}', '${connectorKey}', '${action}', { ${opts.join(", ")} })`,
|
|
195
|
+
"```",
|
|
196
|
+
"",
|
|
197
|
+
];
|
|
135
198
|
}
|
|
136
199
|
/** A compilable `connector(...)` call for this operation; events go to {@link eventExampleLines}. */
|
|
137
200
|
export function exampleLines(j, fields, symbol, invariant, v1def) {
|
|
@@ -162,7 +225,7 @@ export function exampleLines(j, fields, symbol, invariant, v1def) {
|
|
|
162
225
|
const schemaDynamic = pyTruthy(operation.objectFromDiscovery) ||
|
|
163
226
|
pyTruthy(dictOf(operation.schemaAction).parents) ||
|
|
164
227
|
pyTruthy(runtime.requiresConnectionForSchema);
|
|
165
|
-
const out = ["## Example", ""];
|
|
228
|
+
const out = ["## Example", "", "### Flow", ""];
|
|
166
229
|
const pairs = required.map((f) => {
|
|
167
230
|
const [name, , wireName] = authoringField(f);
|
|
168
231
|
return [wireName ? f.name : name, exampleValue(f, symbol, invariant, connectorKey, action)];
|
|
@@ -224,6 +287,7 @@ export function exampleLines(j, fields, symbol, invariant, v1def) {
|
|
|
224
287
|
}
|
|
225
288
|
out.push("");
|
|
226
289
|
}
|
|
290
|
+
out.push(...bpmnExampleLines(required, invariant, connectorKey, action, step, runtime, operation));
|
|
227
291
|
return out;
|
|
228
292
|
}
|
|
229
293
|
/**
|
|
@@ -243,7 +307,7 @@ export function eventExampleLines(j, symbol, connectorKey, action, step, v1def)
|
|
|
243
307
|
.filter((f) => typeof f.name === "string" && !pyTruthy(f.hidden))
|
|
244
308
|
.map((f) => f.name);
|
|
245
309
|
const factory = start ? "onEvent" : "waitForEvent";
|
|
246
|
-
const out = ["## Example", "", "```ts"];
|
|
310
|
+
const out = ["## Example", "", "### Flow", "", "```ts"];
|
|
247
311
|
out.push(`import { ${factory} } from '${PACKAGE_NAME}';`);
|
|
248
312
|
if (symbol)
|
|
249
313
|
out.push(`import { ${symbol} } from './connectors/${connectorKey}.ts';`);
|
|
@@ -291,5 +355,6 @@ export function eventExampleLines(j, symbol, connectorKey, action, step, v1def)
|
|
|
291
355
|
"--connection-id <id>` lists the objects the bound connection exposes.");
|
|
292
356
|
out.push("");
|
|
293
357
|
}
|
|
358
|
+
out.push(...bpmnEventExampleLines(params, generic, start, connectorKey, action, step));
|
|
294
359
|
return out;
|
|
295
360
|
}
|
|
@@ -11,7 +11,7 @@ import { sortedStrings } from "../../../compat/compare.js";
|
|
|
11
11
|
import { pyStr, splitOnce } from "../../../compat/pystr.js";
|
|
12
12
|
import { classify, referenceOf } from "../../../lookup-specs.js";
|
|
13
13
|
import { byAliases } from "../../../naming.js";
|
|
14
|
-
import { resolveFlagArg } from "../../../../core/cli-spelling.js";
|
|
14
|
+
import { prepareCommand, resolveFlagArg } from "../../../../core/cli-spelling.js";
|
|
15
15
|
import { authoringField, referenceCollection, referenceQuery } from "../common.js";
|
|
16
16
|
import { firstValue, IDENT, pyOr, pyTruthy, strList } from "../pyvalue.js";
|
|
17
17
|
/** The `resources run list` call that shows what an identity-lookup field accepts. */
|
|
@@ -94,15 +94,36 @@ export function lookupLines(connectorKey, action, fields, symbol, invariant) {
|
|
|
94
94
|
// array too: the flat key compiles to the same list the nested
|
|
95
95
|
// `recipients: { listIds: [...] }` of `## Example` does.
|
|
96
96
|
const [authoringName, , multiselectWire] = authoringField(firstField);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
else {
|
|
97
|
+
const line = (c, sa) => {
|
|
98
|
+
if (multiselectWire)
|
|
99
|
+
return `${authoringName}: [${c}${sa}]`;
|
|
101
100
|
const key = IDENT.test(firstName) ? firstName : pyJsonString(firstName);
|
|
102
|
-
|
|
103
|
-
}
|
|
101
|
+
return firstName.endsWith("[*]") ? `${key}: [${c}${sa}]` : `${key}: ${c}${sa}`;
|
|
102
|
+
};
|
|
103
|
+
out.push(line(call, sample));
|
|
104
104
|
out.push("```");
|
|
105
105
|
out.push("");
|
|
106
|
+
// A SECOND fence, not a second line in the first: that one is an object
|
|
107
|
+
// literal and `## Example`'s gate compiles it, so two keys there is a
|
|
108
|
+
// syntax error before it is anything else.
|
|
109
|
+
//
|
|
110
|
+
// The descriptor form above is Flow's — `lookup(Descriptor, …)` needs a
|
|
111
|
+
// generated `connectors/<key>.ts` module, and the aliases (`.byName`) exist
|
|
112
|
+
// only on it. BPMN's `.connector()` takes the key and action as strings and
|
|
113
|
+
// imports no descriptor, so a page that printed only the descriptor form
|
|
114
|
+
// handed a BPMN author a call that does not resolve, with the generic
|
|
115
|
+
// `.by()` it needs mentioned two paragraphs further down, after the table.
|
|
116
|
+
if (symbol) {
|
|
117
|
+
const stringCall = `lookup('${connectorKey}', '${action}', '${firstName}')`;
|
|
118
|
+
const generic = firstNames.length ? `.by('${firstNames[0]}', '…')` : ".by('id', '…')";
|
|
119
|
+
out.push("In BPMN there is no descriptor to import, so the key and the action " +
|
|
120
|
+
"are strings and only the generic `.by()` exists:");
|
|
121
|
+
out.push("");
|
|
122
|
+
out.push("```ts");
|
|
123
|
+
out.push(line(stringCall, generic));
|
|
124
|
+
out.push("```");
|
|
125
|
+
out.push("");
|
|
126
|
+
}
|
|
106
127
|
out.push("| field | helpers | matches on | sends |");
|
|
107
128
|
out.push("| --- | --- | --- | --- |");
|
|
108
129
|
for (const [f, reference] of resolvable) {
|
|
@@ -128,7 +149,7 @@ export function lookupLines(connectorKey, action, fields, symbol, invariant) {
|
|
|
128
149
|
out.push("");
|
|
129
150
|
// ONE command carrying every --resolve, and no `--connection-id`.
|
|
130
151
|
out.push("```bash");
|
|
131
|
-
out.push(
|
|
152
|
+
out.push(`${prepareCommand(connectorKey, action)} \\`);
|
|
132
153
|
// The WIRE name (`users[*]`), which is what prepare's lookup table is keyed
|
|
133
154
|
// on — the authoring name (`users`) is refused as RESOLVE_UNKNOWN_FIELD.
|
|
134
155
|
const resolves = resolvable.map(([f, reference]) => {
|
|
@@ -220,7 +220,12 @@ export function unresolvedOperationMessage(input) {
|
|
|
220
220
|
else {
|
|
221
221
|
lines.push("", `\`registry search ${key}\` lists no action for ${key} either.`);
|
|
222
222
|
}
|
|
223
|
-
lines.push("", "`registry get` and `registry search` answer from the CLI's local copy of the catalog.", "If it looks stale, refresh it first: `uip maestro flow registry pull --force`."
|
|
223
|
+
lines.push("", "`registry get` and `registry search` answer from the CLI's local copy of the catalog.", "If it looks stale, refresh it first: `uip maestro flow registry pull --force`.",
|
|
224
|
+
// Named `flow` on purpose, whatever family you are authoring: this is the
|
|
225
|
+
// catalog `prepare` itself reads, and `uip maestro bpmn registry pull` fills
|
|
226
|
+
// a DIFFERENT one (the BPMN extension types). A BPMN author who "corrects"
|
|
227
|
+
// the command here refreshes the wrong cache and sees no change.
|
|
228
|
+
"That is the catalog this command reads, in every family — `uip maestro bpmn registry`", "is the BPMN extension-type catalog, a different cache, and pulling it changes nothing here.");
|
|
224
229
|
return lines.join("\n");
|
|
225
230
|
}
|
|
226
231
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const IMPORT_PATTERN: RegExp;
|
|
6
6
|
/**
|
|
7
|
-
* Where
|
|
7
|
+
* Where an authored source will not be: generated output and installed packages.
|
|
8
8
|
* Matched by name at any depth, so `.flow-sdk/connectors/` is skipped too while
|
|
9
9
|
* a source kept in `.flow-sdk/` itself is found.
|
|
10
10
|
*/
|
|
@@ -22,10 +22,10 @@ export declare function isRelativeSpecifier(specifier: string): boolean;
|
|
|
22
22
|
* A bare package specifier never does — it is not our overlay.
|
|
23
23
|
*/
|
|
24
24
|
export declare function resolvesTo(specifier: string, source: string, target: string): boolean;
|
|
25
|
-
/** Every
|
|
25
|
+
/** Every authored source file under `root`, sorted, skipping the directories a source is never in. */
|
|
26
26
|
export declare function flowSources(root: string): string[];
|
|
27
27
|
/**
|
|
28
|
-
* `_sources_importing`: every
|
|
28
|
+
* `_sources_importing`: every authored source under `root` whose import resolves
|
|
29
29
|
* to `baked`. Selection is by CONTENT rather than "the single flow in this
|
|
30
30
|
* directory": a workspace with three flows, one of which uses this connector,
|
|
31
31
|
* has one correct answer and this finds it.
|
|
@@ -31,7 +31,7 @@ import { repointedLine, severalSourcesLine, sourceNotAFileLine } from "./message
|
|
|
31
31
|
*/
|
|
32
32
|
export const IMPORT_PATTERN = /(^\s*import\s[^;'"]*?\sfrom\s*)(['"])([^'"]+)(\2)/gm;
|
|
33
33
|
/**
|
|
34
|
-
* Where
|
|
34
|
+
* Where an authored source will not be: generated output and installed packages.
|
|
35
35
|
* Matched by name at any depth, so `.flow-sdk/connectors/` is skipped too while
|
|
36
36
|
* a source kept in `.flow-sdk/` itself is found.
|
|
37
37
|
*/
|
|
@@ -75,7 +75,18 @@ function comparePaths(a, b) {
|
|
|
75
75
|
}
|
|
76
76
|
return a.length - b.length;
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* The authored-source suffixes `prepare` reads.
|
|
80
|
+
*
|
|
81
|
+
* `.bpmn.ts` is here because a BPMN `.connector()` binds a connection and a
|
|
82
|
+
* folder exactly as a Flow step does, and `prepare` reuses the author's existing
|
|
83
|
+
* label names rather than inventing new ones (`source-labels.ts`). It will never
|
|
84
|
+
* match in `sourcesImporting` — BPMN takes its connector as strings and imports
|
|
85
|
+
* no descriptor — which costs one `readFileSync` per BPMN source and keeps the
|
|
86
|
+
* two callers on one walk.
|
|
87
|
+
*/
|
|
88
|
+
const SOURCE_SUFFIXES = ['.flow.ts', '.bpmn.ts'];
|
|
89
|
+
/** Every authored source file under `root`, sorted, skipping the directories a source is never in. */
|
|
79
90
|
export function flowSources(root) {
|
|
80
91
|
const found = [];
|
|
81
92
|
const walk = (dir, parts) => {
|
|
@@ -91,7 +102,7 @@ export function flowSources(root) {
|
|
|
91
102
|
if (!SOURCE_SKIP_DIRS.has(child.name))
|
|
92
103
|
walk(join(dir, child.name), [...parts, child.name]);
|
|
93
104
|
}
|
|
94
|
-
else if (child.isFile() && child.name.endsWith(
|
|
105
|
+
else if (child.isFile() && SOURCE_SUFFIXES.some((suffix) => child.name.endsWith(suffix))) {
|
|
95
106
|
found.push([...parts, child.name]);
|
|
96
107
|
}
|
|
97
108
|
}
|
|
@@ -101,7 +112,7 @@ export function flowSources(root) {
|
|
|
101
112
|
return found.map((parts) => join(root, ...parts));
|
|
102
113
|
}
|
|
103
114
|
/**
|
|
104
|
-
* `_sources_importing`: every
|
|
115
|
+
* `_sources_importing`: every authored source under `root` whose import resolves
|
|
105
116
|
* to `baked`. Selection is by CONTENT rather than "the single flow in this
|
|
106
117
|
* directory": a workspace with three flows, one of which uses this connector,
|
|
107
118
|
* has one correct answer and this finds it.
|