@uipath/maestro-builder-sdk 6.70.9 → 6.71.1

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.
@@ -0,0 +1,162 @@
1
+ import { missingBindingLabels } from '../../../core/binding-messages.js';
2
+ import { prepareCommand } from '../../../core/cli-spelling.js';
3
+ import { commentText } from '../../../core/comment-text.js';
4
+ import { eventFilterFromTreeLeaf } from '../../../core/event-filters.js';
5
+ import { eventDetail, eventTarget, libraryRequiredForEvent, refuseUnverifiedByoa, resolveEvent } from '../../../core/event-subscription.js';
6
+ import { Expr } from '../../../core/expr.js';
7
+ import { tsLiteral, tsObject, tsOptions } from '../../../core/ts-literal.js';
8
+ const EVENT_PREFIX = 'uipath.connector.event.';
9
+ /** `<connector>.<event>` after the prefix, split on the last dot; undefined for a name with no event. */
10
+ export function splitEvent(typeName) {
11
+ if (!typeName.startsWith(EVENT_PREFIX))
12
+ return undefined;
13
+ const rest = typeName.slice(EVENT_PREFIX.length);
14
+ const dot = rest.lastIndexOf('.');
15
+ if (dot <= 0)
16
+ return undefined;
17
+ return { connector: rest.slice(0, dot), event: rest.slice(dot + 1) };
18
+ }
19
+ function serializeEventWait(spec, ctx) {
20
+ const sub = spec.subscription;
21
+ const site = `waitForEvent "${ctx.step}"`;
22
+ if (!ctx.library)
23
+ throw libraryRequiredForEvent(ctx.events, sub, ctx.nodeId, site, site);
24
+ const rc = resolveEvent(ctx.library, 'event', sub, site);
25
+ refuseUnverifiedByoa(ctx.events, site, sub);
26
+ const detail = eventDetail(rc, sub, ctx.nodeId, ctx.events, site);
27
+ // The platform reaches the connection through the flow's `bindings[]`, as for
28
+ // a connector operation (board G-19). `eventDetail` refused a node without
29
+ // both labels, so both resolve here.
30
+ const target = eventTarget(sub);
31
+ const connectionId = ctx.resolveBinding(sub.connection, 'connection', site, target);
32
+ const folderKey = ctx.resolveBinding(sub.folder, 'folder', site, target);
33
+ ctx.bindConnection({
34
+ model: rc.v1def?.model,
35
+ connectorKey: rc.connectorKey, connectionId: connectionId,
36
+ ...(folderKey !== undefined ? { folderKey } : {}),
37
+ });
38
+ // The node is the event the author named (`rc.nodeType`); a definition naming
39
+ // another is written under that name, so the node and its definitions[] entry agree.
40
+ const v1def = rc.v1def;
41
+ const def = v1def.nodeType === rc.nodeType ? v1def : { ...v1def, nodeType: rc.nodeType };
42
+ return {
43
+ definition: { source: 'snapshot', nodeType: def.nodeType, version: def.version, manifest: def },
44
+ inputs: { detail },
45
+ // A wait continues on `output`, as every connector node does.
46
+ port: 'output',
47
+ };
48
+ }
49
+ /** A `where` value as decompile reads it: an expression back to an `Expr`, wire text as text, any other JSON as is. */
50
+ function liftValue(v, ctx) {
51
+ const raw = Array.isArray(v) ? undefined : ctx.sourceText(v);
52
+ if (raw === undefined)
53
+ return v;
54
+ return raw.startsWith('=js:') ? new Expr(raw.slice(4)) : raw;
55
+ }
56
+ /**
57
+ * The subscription back from the node: the `where` scope from
58
+ * `detail.eventParameters`, which carries it verbatim, and the filters from the
59
+ * filter tree inside `detail.configuration`. Both are also encoded into the
60
+ * generated `filterExpression`, but reading the structured copies avoids parsing
61
+ * a string compile generated.
62
+ */
63
+ function liftEventWait(node, ctx) {
64
+ const n = node;
65
+ const split = splitEvent(n.type);
66
+ if (!split)
67
+ return undefined;
68
+ const detail = (n.inputs?.detail ?? undefined);
69
+ const where = Object.entries((detail?.eventParameters ?? {}))
70
+ .filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
71
+ .map(([k, v]) => [k, liftValue(v, ctx)]);
72
+ // The filter tree lives inside the `=jsonString:` configuration blob, and so
73
+ // does the one fact that says whether `detail.objectName` was the AUTHOR's
74
+ // choice: a GENERIC event (`activityType: GenericTrigger`) carries the object
75
+ // the author named, a curated one carries its built-in object.
76
+ let filters = [];
77
+ let generic = false;
78
+ const config = detail?.configuration;
79
+ if (typeof config === 'string' && config.startsWith('=jsonString:')) {
80
+ try {
81
+ const parsed = JSON.parse(config.slice('=jsonString:'.length));
82
+ generic = parsed?.essentialConfiguration?.instanceParameters?.activityType === 'GenericTrigger';
83
+ const rows = (parsed?.essentialConfiguration?.filter?.filters ?? []);
84
+ filters = rows
85
+ .map((f) => eventFilterFromTreeLeaf(f))
86
+ .filter((f) => f !== undefined);
87
+ }
88
+ catch {
89
+ // A hand-edited blob is not ours to guess at — the scope and identity
90
+ // still recover, and the filters simply do not come back.
91
+ filters = [];
92
+ }
93
+ }
94
+ const object = generic && typeof detail?.objectName === 'string' && detail.objectName !== ''
95
+ ? detail.objectName
96
+ : undefined;
97
+ const connection = typeof detail?.connectionId === 'string' ? detail.connectionId : undefined;
98
+ const folder = typeof detail?.connectionFolderKey === 'string' ? detail.connectionFolderKey : undefined;
99
+ // A connection the file never picked (`connectionId: null`): the source cannot
100
+ // invent labels for it, and compile and check refuse a node without both
101
+ // (BINDING_MISSING, flow-builder-sdk#890), so the source says so at the node
102
+ // and the run warns once.
103
+ const missing = missingBindingLabels({ connection, folder });
104
+ if (missing.length > 0) {
105
+ ctx.warn(`node ${tsLiteral(n.id)} (${n.type}) has no ${missing.join(' or ')} in the .flow (an unbound draft). `
106
+ + `The source leaves ${missing.length > 1 ? 'them' : 'it'} out, and compile and check refuse it (BINDING_MISSING) `
107
+ + `until it names both labels: run \`${prepareCommand(split.connector, split.event)}\`, then pass `
108
+ + `\`connection\` and \`folder\`.`);
109
+ }
110
+ return { kind: 'waitEvent', ...split, where, object, filters, connection, folder, missing };
111
+ }
112
+ /**
113
+ * `waitForEvent({ … })`, the stringly form: the subscription as one object. The
114
+ * unbound marker sits inside the call, after the object, where the lift has
115
+ * always put it.
116
+ */
117
+ function printEventWait(l, ctx) {
118
+ const value = (v) => (v instanceof Expr ? ctx.value(v) : ctx.json(v));
119
+ const filters = l.filters.map((f) => tsOptions([
120
+ ['field', tsLiteral(f.field)],
121
+ [f.operator, typeof f.value === 'string' ? tsLiteral(f.value) : JSON.stringify(f.value)],
122
+ ]));
123
+ const subscription = tsOptions([
124
+ ['connector', tsLiteral(l.connector)],
125
+ ['event', tsLiteral(l.event)],
126
+ ['where', l.where.length ? tsObject(l.where.map(([k, v]) => [k, value(v)])) : undefined],
127
+ ['object', l.object !== undefined ? tsLiteral(l.object) : undefined],
128
+ ['filters', filters.length ? `[${filters.join(', ')}]` : undefined],
129
+ ['connection', l.connection !== undefined ? tsLiteral(l.connection) : undefined],
130
+ ['folder', l.folder !== undefined ? tsLiteral(l.folder) : undefined],
131
+ ]);
132
+ const marker = l.missing.length > 0
133
+ ? ` /* TODO: bind ${commentText(l.missing.join(' and '))}; check refuses this node until it has both labels (BINDING_MISSING) */`
134
+ : '';
135
+ return { factory: 'waitForEvent', positional: [subscription + marker] };
136
+ }
137
+ const eventWait10 = {
138
+ kind: 'waitEvent', typeName: `${EVENT_PREFIX}*`, version: '1.0.0', factory: 'waitForEvent',
139
+ definition: 'per-tenant',
140
+ // An event wait resumes or it does not: it has no error port to route.
141
+ errorPort: false,
142
+ serialize: (spec, ctx) => serializeEventWait(spec, ctx),
143
+ deserialize: (node, ctx) => liftEventWait(node, ctx),
144
+ print: (spec, ctx) => printEventWait(spec, ctx),
145
+ check: (spec, ctx) => ctx.eventSubscription(spec.subscription, `waitForEvent "${ctx.step}"`),
146
+ namedNode: (spec) => {
147
+ const sub = spec.subscription;
148
+ return {
149
+ typeName: `${EVENT_PREFIX}${sub?.connector}.${sub?.event}`,
150
+ ...(typeof sub?.version === 'string' ? { version: sub.version } : {}),
151
+ };
152
+ },
153
+ expressions: () => [],
154
+ output: () => ({ kind: 'opaque' }),
155
+ };
156
+ export const eventWaitNodeType = {
157
+ kind: 'waitEvent', typeName: `${EVENT_PREFIX}*`, typeNamePrefix: EVENT_PREFIX, factory: 'waitForEvent',
158
+ typeNameMatches: (typeName) => splitEvent(typeName) !== undefined,
159
+ versionedByDefinition: true,
160
+ defaultVersion: eventWait10.version,
161
+ versions: { [eventWait10.version]: eventWait10 },
162
+ };
@@ -1,3 +1,3 @@
1
- /** The connector node family, as flow-node-registry.ts lists it: an Integration Service connector operation. */
1
+ /** The connector node family, as flow-node-registry.ts lists it: an Integration Service connector operation, and a mid-flow event wait. */
2
2
  import type { FlowNodeFamily } from '../../../flow-node-type.js';
3
3
  export declare const connectorFlowFamily: FlowNodeFamily;
@@ -1,8 +1,9 @@
1
+ import { eventWaitNodeType } from './event-wait.js';
1
2
  import { connectorNodeType } from './node-type.js';
2
3
  export const connectorFlowFamily = {
3
4
  id: 'connector',
4
5
  title: 'Integration Service connector',
5
- types: [connectorNodeType],
6
- // Its rules' codes are the pipeline's, shared with an event subscription (ctx.connector).
6
+ types: [connectorNodeType, eventWaitNodeType],
7
+ // Its rules' codes are the pipeline's, shared with the triggers (ctx.connector, ctx.eventSubscription).
7
8
  diagnostics: {},
8
9
  };
@@ -33,6 +33,9 @@ export const PROCESS_DIAGNOSTICS = {
33
33
  AGENTIC_RESERVED_INPUT: { severity: 'error', summary: 'an agentic process step with the reserved fireAndForget input' },
34
34
  APIWF_EMPTY_RETURNS: { severity: 'warning', summary: 'an API workflow step with returns: {}' },
35
35
  APIWF_KEY_NOT_KEY: { severity: 'error', summary: 'an API workflow step whose key is not a GUID' },
36
+ APIWF_LOCAL_HAS_FOLDER: { severity: 'error', summary: 'an API workflow step in-solution, with a folderPath' },
37
+ APIWF_LOCAL_NO_PROJECT_ID: { severity: 'error', summary: 'an API workflow step in-solution, with no projectId' },
38
+ APIWF_LOCAL_PROJECT_ID_NOT_GUID: { severity: 'error', summary: 'an API workflow step in-solution, with a projectId that is not a GUID' },
36
39
  APIWF_NAME_IS_PATH: { severity: 'error', summary: 'an API workflow step whose name looks like a folder path' },
37
40
  APIWF_NO_FOLDER: { severity: 'error', summary: 'an API workflow step with no folderPath' },
38
41
  APIWF_NO_KEY: { severity: 'error', summary: 'an API workflow step with no key' },
@@ -157,10 +160,21 @@ export function checkApiWorkflow(inputs, ctx) {
157
160
  at('APIWF_NO_NAME', `API workflow step "${step}" has no \`name\`. The runtime starts the job by name + folder, not by the `
158
161
  + `key in the node type, so without it the workflow cannot be resolved and the run fails before dispatch.`, `name: 'NameToAgeFixed'`);
159
162
  }
160
- if (blank(inputs?.folderPath)) {
163
+ const inSolution = inputs?.location === 'in-solution';
164
+ if (!inSolution && blank(inputs?.folderPath)) {
161
165
  at('APIWF_NO_FOLDER', `API workflow step "${step}" has no \`folderPath\`. The runtime starts the job by name + folder, so the `
162
- + `Orchestrator folder holding the workflow is required.`, `folderPath: 'Shared'`);
166
+ + `Orchestrator folder holding the workflow is required. For an API workflow in the SAME solution, set `
167
+ + `\`location: 'in-solution'\` and its \`projectId\` instead.`, `folderPath: 'Shared'`);
168
+ }
169
+ if (inSolution && blank(inputs?.projectId)) {
170
+ at('APIWF_LOCAL_NO_PROJECT_ID', `In-solution API workflow step "${step}" has no \`projectId\`. Read \`resource.projectKey\` from `
171
+ + `\`resources/solution_folder/process/api/<project>.json\`.`, `projectId: '<resource.projectKey>'`);
172
+ }
173
+ else if (inSolution && !GUID_RE.test(String(inputs.projectId).trim())) {
174
+ at('APIWF_LOCAL_PROJECT_ID_NOT_GUID', `In-solution API workflow step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
175
+ + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
163
176
  }
177
+ // A folderPath beside in-solution is the serializer's APIWF_LOCAL_HAS_FOLDER (refusals.ts).
164
178
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
165
179
  if (name.includes('/')) {
166
180
  at('APIWF_NAME_IS_PATH', `API workflow step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. `
@@ -176,7 +176,8 @@ const API_WORKFLOW_PREFIX = 'uipath.core.api-workflow.';
176
176
  * one the platform's own definition writes.
177
177
  */
178
178
  function apiWorkflowDef(spec) {
179
- const resourceKey = `${spec.folderPath}.${spec.name}`;
179
+ const loc = processResourceLocation(spec);
180
+ const { resourceKey } = loc;
180
181
  const properties = {};
181
182
  for (const [k, v] of Object.entries(spec.inputs)) {
182
183
  properties[k] = { type: typeof v === 'number' ? 'number' : typeof v === 'boolean' ? 'boolean' : 'string' };
@@ -186,8 +187,8 @@ function apiWorkflowDef(spec) {
186
187
  version: '1.0.0',
187
188
  category: 'api-workflow',
188
189
  runtimeConstraints: { exclude: ['api-function'] },
189
- description: `(${spec.folderPath})`,
190
- tags: [],
190
+ description: loc.description,
191
+ tags: loc.tags,
191
192
  sortOrder: resourceNodeSortOrder('api-workflow'),
192
193
  supportsErrorHandling: true,
193
194
  display: { label: spec.name, icon: 'api' },
@@ -195,11 +196,12 @@ function apiWorkflowDef(spec) {
195
196
  { position: 'left', handles: [{ id: 'input', type: 'target', handleType: 'input' }] },
196
197
  { position: 'right', handles: [{ id: 'output', type: 'source', handleType: 'output' }] },
197
198
  ],
199
+ ...loc.definitionExtras,
198
200
  model: {
199
201
  type: 'bpmn:ServiceTask',
200
202
  serviceType: 'Orchestrator.ExecuteApiWorkflowAsync',
201
203
  version: 'v2',
202
- section: 'Published',
204
+ ...(loc.inSolution ? {} : { section: 'Published' }),
203
205
  bindings: {
204
206
  resource: 'process',
205
207
  resourceSubType: 'Api',
@@ -207,9 +209,10 @@ function apiWorkflowDef(spec) {
207
209
  orchestratorType: 'api',
208
210
  values: [
209
211
  { name: 'name', propertyAttribute: 'name', default: spec.name },
210
- { name: 'folderPath', propertyAttribute: 'folderPath', default: spec.folderPath },
212
+ { name: 'folderPath', propertyAttribute: 'folderPath', default: loc.folderPath },
211
213
  ],
212
214
  },
215
+ ...loc.modelExtras,
213
216
  context: [
214
217
  { name: 'name', type: 'string', value: '<bindings.name>' },
215
218
  { name: 'folderPath', type: 'string', value: '<bindings.folderPath>' },
@@ -611,7 +614,10 @@ export function processDefinition(spec) {
611
614
  inputs: s.inputs ?? {}, returns: s.returns,
612
615
  });
613
616
  case 'apiWorkflow':
614
- return apiWorkflowDef({ key: s.key, name: s.name, folderPath: s.folderPath, inputs: s.inputs ?? {}, returns: s.returns });
617
+ return apiWorkflowDef({
618
+ key: s.key, name: s.name, folderPath: s.folderPath, location: s.location, projectId: s.projectId,
619
+ inputs: s.inputs ?? {}, returns: s.returns,
620
+ });
615
621
  case 'agenticProcess':
616
622
  return agenticProcessDef({
617
623
  key: s.key, name: s.name, folderPath: s.folderPath, location: s.location, projectId: s.projectId,
@@ -723,11 +729,14 @@ function liftProcess(node, ctx, prefix, kind, form) {
723
729
  const name = binding('name');
724
730
  const folderPath = binding('folderPath');
725
731
  // A sibling project of the same solution (#951): the model names its project
726
- // instead of a folder. `isInSolution` is flow-core's marker. Read back as
727
- // `location: 'in-solution'` so the recompile takes the in-solution path
728
- // instead of a published one with an empty folder.
732
+ // instead of a folder. `isInSolution` is flow-core's marker; files that carry
733
+ // `projectName` but neither it nor `projectId` still carry the `in-solution`
734
+ // tag and an empty folder. Read back as `location: 'in-solution'` so the
735
+ // recompile takes the in-solution path instead of a published one with an
736
+ // empty folder (#1396). A tag beside a real folder is not enough.
729
737
  const projectId = typeof def?.model?.projectId === 'string' ? def.model.projectId : undefined;
730
- const inSolution = def?.model?.isInSolution === true || projectId !== undefined;
738
+ const inSolution = def?.model?.isInSolution === true || projectId !== undefined
739
+ || (Array.isArray(def?.tags) && def.tags.includes('in-solution') && !folderPath);
731
740
  const inputs = { ...(n.inputs ?? {}) };
732
741
  // The design-time fire-and-forget switch is an OPTION on agenticProcess(),
733
742
  // not a process argument — serialize adds it to `inputs` after rendering the
@@ -25,7 +25,7 @@ const KINDS = {
25
25
  noKeySuggestion: `key: '486edc26-0658-4ac1-92c9-1ef953927151'`,
26
26
  },
27
27
  apiWorkflow: {
28
- noKeyCode: 'APIWF_NO_KEY', label: 'API workflow', oldField: 'workflow',
28
+ noKeyCode: 'APIWF_NO_KEY', localCode: 'APIWF_LOCAL_HAS_FOLDER', label: 'API workflow', oldField: 'workflow', localLabel: 'API workflow step',
29
29
  noKey: (step) => `API workflow step "${step}" has no \`key\`. That is the published workflow's key (a GUID), and `
30
30
  + `it becomes part of the node's type — without it there is no workflow to invoke. Find it with `
31
31
  + `\`uip or processes list --all-folders\` (an API workflow's ProcessKey reads "<name>.api.<name>").`,
@@ -74,12 +74,38 @@ export interface ApiWorkflowInputs {
74
74
  * `uip or processes list --all-folders --process-type Api` (an API workflow's
75
75
  * `ProcessKey` reads `<name>.api.<name>`; the GUID you want is the row's `Key`)
76
76
  * or `uip maestro flow registry search`.
77
+ *
78
+ * For an in-solution sibling it is `resource.key` from
79
+ * `resources/solution_folder/process/api/<project>.json`, the suffix of the
80
+ * node type `uip maestro flow registry list --local` shows.
77
81
  */
78
82
  key: string;
79
83
  /** The workflow's name in Orchestrator — e.g. `'NameToAgeFixed'`. */
80
84
  name: string;
81
- /** The Orchestrator folder holding it — e.g. `'Shared'`. */
82
- folderPath: string;
85
+ /**
86
+ * Where the workflow lives. `'published'` (the default) is a workflow
87
+ * deployed to `folderPath`. `'in-solution'` is a sibling API workflow project
88
+ * of the same solution: it needs `projectId` and no `folderPath`, and `key` is
89
+ * that sibling's `resource.key`.
90
+ */
91
+ location?: ProcessLocation;
92
+ /**
93
+ * The Orchestrator folder holding it — e.g. `'Shared'`. Required for a
94
+ * published workflow; omit it for an in-solution sibling.
95
+ *
96
+ * @enforcedBy APIWF_NO_FOLDER A published API workflow needs its folder.
97
+ * @enforcedBy APIWF_LOCAL_HAS_FOLDER An in-solution API workflow has no folder.
98
+ */
99
+ folderPath?: string;
100
+ /**
101
+ * The sibling API workflow project's `resource.projectKey`, read from
102
+ * `resources/solution_folder/process/api/<project>.json`. Required only when
103
+ * `location` is `'in-solution'`.
104
+ *
105
+ * @enforcedBy APIWF_LOCAL_NO_PROJECT_ID An in-solution API workflow needs its project id.
106
+ * @enforcedBy APIWF_LOCAL_PROJECT_ID_NOT_GUID The project id is the sibling's GUID, not its name.
107
+ */
108
+ projectId?: string;
83
109
  /**
84
110
  * The WORKFLOW's own input arguments, by its own argument names (e.g.
85
111
  * `{ name: 'tomasz' }`). Not a fixed schema — every workflow declares its own.