@uipath/maestro-builder-sdk 6.37.0 → 6.39.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.
@@ -163,6 +163,10 @@ export type BpmnNode = {
163
163
  inputs: Record<string, unknown>;
164
164
  /** Variable the response lands in; defaults to `<id>_response`. */
165
165
  outputVar?: string;
166
+ /** Variable the error payload lands in; defaults to `<id>_Error`. */
167
+ errorVar?: string;
168
+ /** Extra mapped output rows, by the variable each writes — see `BpmnConnectorOpts.outputs`. */
169
+ outputs?: Record<string, string | ConnectorOutputRow>;
166
170
  /** `=`-expression that, when true, skips this activity (`uipath:activity/@skipCondition`). */
167
171
  skipCondition?: string;
168
172
  }) | (ActivityNodeFields & {
@@ -3140,6 +3144,20 @@ declare abstract class ScopeBuilder {
3140
3144
  vars(entries: VarEntry[]): this;
3141
3145
  private declareVar;
3142
3146
  }
3147
+ /**
3148
+ * One extra output row on a connector task, spelled out — the object form of an entry in
3149
+ * {@link BpmnConnectorOpts}' `outputs`.
3150
+ */
3151
+ export interface ConnectorOutputRow {
3152
+ /** The `=`-expression against the result, e.g. `'=response.Id'`. */
3153
+ source: string;
3154
+ /** The row's `name`; defaults to the variable. The designer names a row after the field it reads. */
3155
+ name?: string;
3156
+ /** The row's `type`; defaults to the variable's declared type, else `string`. */
3157
+ type?: string;
3158
+ /** The designer's `custom="true"` mark on a row the author added in the variables panel. */
3159
+ custom?: boolean;
3160
+ }
3143
3161
  /**
3144
3162
  * `.connector()` options: the shared connector options, plus the BPMN-only
3145
3163
  * display `name` and output-variable override.
@@ -3164,6 +3182,21 @@ export type BpmnConnectorOpts = ConnectorOpts & ActivityOpts & {
3164
3182
  * always lands in `<id>_Error`.
3165
3183
  */
3166
3184
  outputVar?: string;
3185
+ /**
3186
+ * Variable the error payload lands in. Defaults to `<id>_Error`, the name every
3187
+ * `.connector()` has always written; an import names whatever the file bound
3188
+ * (`error12`), so decompile can lift a connector task without renaming its variable.
3189
+ */
3190
+ errorVar?: string;
3191
+ /**
3192
+ * Extra output rows beside the response and the Error, by the variable each
3193
+ * writes: `{ invoiceId: '=response.Id' }` writes `<uipath:output name="invoiceId"
3194
+ * source="=response.Id" var="invoiceId"/>`; the object form names and types the row
3195
+ * (`{ id5: { source: '=response.Id', name: 'Id', type: 'string' } }`), which is how a
3196
+ * designer-added row reads back from an import. Each variable is declared for you
3197
+ * unless you declared it.
3198
+ */
3199
+ outputs?: Record<string, string | ConnectorOutputRow>;
3167
3200
  };
3168
3201
  /** A sub-process body: the same graph methods, plus an internal node builder. */
3169
3202
  export declare class SubProcessBuilder extends ScopeBuilder {
@@ -943,9 +943,13 @@ class ScopeBuilder {
943
943
  object: spec.object,
944
944
  inputs: spec.inputs,
945
945
  outputVar: bpmnOpts.outputVar,
946
+ errorVar: bpmnOpts.errorVar,
947
+ outputs: bpmnOpts.outputs,
946
948
  skipCondition: bpmnOpts.skipCondition,
949
+ tags: bpmnOpts.tags,
947
950
  retry: bpmnOpts.retry,
948
951
  loop: bpmnOpts.loop,
952
+ errorMapping: bpmnOpts.errorMapping,
949
953
  }, body);
950
954
  }
951
955
  /**
@@ -67,6 +67,7 @@ export function check(built, opts = {}) {
67
67
  checkVariableOrder(built, diags);
68
68
  checkDocumentIds(built, diags);
69
69
  checkFolderBindings(built, diags);
70
+ checkIntsvcActivities(built, diags);
70
71
  checkEntryPointId(built, diags);
71
72
  checkMethodParentheses(built, diags);
72
73
  checkConnectorRows(built, diags);
@@ -99,6 +100,21 @@ function checkConnectors(built, opts, diags) {
99
100
  checkConnectorLookups(where, n, opts, EQUALS_EXPR_DIALECT, report);
100
101
  checkConnectorSchema(where, n, opts, EQUALS_EXPR_DIALECT, report);
101
102
  checkConnectorContract(where, n, opts, report);
103
+ // Two rows writing one variable: an `outputs` key that is also the response or
104
+ // error variable. Both rows would be emitted and the later write wins at run time.
105
+ const response = n.outputVar ?? `${n.id}_response`;
106
+ const error = n.errorVar ?? `${n.id}_Error`;
107
+ for (const varId of Object.keys(n.outputs ?? {})) {
108
+ if (varId !== response && varId !== error)
109
+ continue;
110
+ report({
111
+ level: 'error',
112
+ code: 'CONNECTOR_OUTPUT_VAR_CLASH',
113
+ message: `${where}: outputs maps "${varId}", which is already the task's ${varId === response ? 'response' : 'error'} variable ` +
114
+ `(\`${varId === response ? 'outputVar' : 'errorVar'}\`); two rows would write it. Map the extra row to another variable, ` +
115
+ `or drop it and read \`vars.${varId}\` directly.`,
116
+ });
117
+ }
102
118
  }
103
119
  }
104
120
  /**
@@ -365,6 +381,242 @@ function checkFolderBindings(built, diags) {
365
381
  }
366
382
  }
367
383
  }
384
+ /** The `method` vocabulary of the Integration Service activity catalog (`MethodName`). */
385
+ const INTSVC_METHODS = new Set(['GET', 'GETBYID', 'POST', 'PUT', 'PATCH', 'DELETE']);
386
+ /** The catalog's identity fields on an `Intsvc.ActivityExecution` task, in the order the message names them. */
387
+ const INTSVC_IDENTITY = ['connectorKey', 'objectName', 'operation', 'method', 'path'];
388
+ /**
389
+ * A hand-filled `Intsvc.ActivityExecution` task: what the catalog, the refresh and
390
+ * the runtime require of its context that `uip maestro bpmn validate` does not report.
391
+ *
392
+ * `.connector()` and `.dataService()` write this task from the library; an author
393
+ * who reaches for `.activity(id, 'Intsvc.ActivityExecution', …)` — the route the skill
394
+ * names for a connector the library lacks — fills the identity rows by hand from
395
+ * `uip is activities list <key>`, and the 2026-10-16 eval run measured what goes wrong
396
+ * (141 SDK-authored files, 109 such tasks): `objectName: 'N/A'` and `method: 'N/A'`
397
+ * copied from the catalog's GENERIC rows (5 tasks — `validate` reported them Valid, and
398
+ * they dispatch to nothing), an empty `operation` (5), `<METHOD_FROM_CLI>`-style template
399
+ * placeholders left in place, and a `connection` that `uip maestro bpmn refresh` then
400
+ * refused with `connection must reference a root binding as "=bindings.<id>"`.
401
+ *
402
+ * What the registry's own field notes say the rows are: `operation` is the catalog
403
+ * activity's `Name`, `objectName` its `ObjectName`, `method` its `MethodName`, `path` the
404
+ * request path `is resources describe` returns, and `connection` "points to a root binding
405
+ * for a Connection resource" with `folderKey` a second binding for the same key. Real
406
+ * exports agree on the shape and differ on one spelling: `operation` is the catalog Name
407
+ * on the canvas (`UpdateEntityRecordV2`) and the CRUD verb in 94 of 108 hand-written v1
408
+ * tasks (`Create`, `Retrieve`), so that field is held to "an identifier", not a list.
409
+ * `method` IS a closed list: GET, GETBYID, POST, PUT, PATCH, DELETE, measured over every
410
+ * task in both arms and the corpus.
411
+ *
412
+ * Errors where the task cannot dispatch or the platform refuses it; warnings where it
413
+ * deploys but the SDK cannot tell it is right.
414
+ */
415
+ function checkIntsvcActivities(built, diags) {
416
+ // The bindings the compiled process will carry: the ones declared with `.binding()`,
417
+ // and the ones `serialize` synthesizes for every `.connector()`, `.eventTrigger()` /
418
+ // `.waitForEvent()` and external task from the symbolic names they read (ids equal
419
+ // to the names, `connector.ts` `bindingRef`) — a hand-filled task may reuse those.
420
+ const declared = new Map(built.bindings.map((b) => [b.id, { resource: String(b.resource ?? ''), propertyAttribute: String(b.propertyAttribute ?? '') }]));
421
+ for (const n of allNodes(built.nodes)) {
422
+ if (n.kind !== 'connector' && n.kind !== 'connectorEvent' && n.kind !== 'externalTask')
423
+ continue;
424
+ if (n.connection && !declared.has(n.connection))
425
+ declared.set(n.connection, { resource: 'Connection', propertyAttribute: 'ConnectionId' });
426
+ if (n.folder && !declared.has(n.folder))
427
+ declared.set(n.folder, { resource: 'Connection', propertyAttribute: 'folderKey' });
428
+ }
429
+ const expr = (v) => typeof v === 'string' && v.startsWith('=');
430
+ const text = (v) => (typeof v === 'string' ? v : v === undefined || v === null ? undefined : String(v));
431
+ for (const n of allNodes(built.nodes)) {
432
+ if (n.kind !== 'typed' || n.type !== 'Intsvc.ActivityExecution')
433
+ continue;
434
+ // `contextRows`, when given, REPLACE `context` at emission (typed-node.ts), so only
435
+ // the rows are the task's context then; a record field beside them is not emitted.
436
+ const ctx = {};
437
+ if (n.contextRows)
438
+ for (const row of n.contextRows)
439
+ ctx[row.name] = row.value ?? row.body ?? '';
440
+ else
441
+ Object.assign(ctx, n.context);
442
+ const where = n.id;
443
+ const key = text(ctx['connectorKey']) ?? '<connector-key>';
444
+ // 1. A catalog placeholder: `N/A` from a generic row, or a template's `<…>`.
445
+ for (const field of INTSVC_IDENTITY) {
446
+ const v = text(ctx[field]);
447
+ if (v === undefined)
448
+ continue;
449
+ const placeholder = /^n\/a$/i.test(v.trim()) || /^(<|&lt;)[^<>]*(>|&gt;)$/.test(v.trim());
450
+ if (!placeholder)
451
+ continue;
452
+ diags.push({
453
+ level: 'error',
454
+ code: 'INTSVC_CATALOG_PLACEHOLDER',
455
+ message: `"${n.id}" (Intsvc.ActivityExecution) has ${field}="${v}", which dispatches to nothing. ` +
456
+ (/^n\/a$/i.test(v.trim())
457
+ ? `"N/A" is what \`uip is activities list ${key}\` prints on a GENERIC record verb (Create/Retrieve/Update/Delete Record), ` +
458
+ `whose object is chosen at design time: set objectName to an object from \`uip is resources list ${key} --connection-id <id>\`, ` +
459
+ `method to that verb's HTTP method, and path from \`uip is resources describe ${key} <object> --operation <Verb> --connection-id <id>\`. ` +
460
+ `For a curated activity, objectName and method are the row's ObjectName and MethodName.`
461
+ : `Replace the placeholder with the catalog's value (\`uip is activities list ${key} --output json\`: Name → operation, ObjectName → objectName, MethodName → method; ` +
462
+ `\`uip is resources describe\` → path).`) +
463
+ ` Or let the library write the task: \`uip maestro registry prepare ${key} --action <name>\` then \`.connector()\`.`,
464
+ where,
465
+ });
466
+ }
467
+ // 2. `method` is the catalog's closed vocabulary.
468
+ const method = text(ctx['method']);
469
+ // An empty one is EMPTY_REQUIRED_FIELD's (the registry marks it required), as for `operation`.
470
+ if (method !== undefined && method.trim() !== '' && !expr(method) && !/^n\/a$/i.test(method.trim()) && !/^(<|&lt;)/.test(method.trim()) && !INTSVC_METHODS.has(method.trim())) {
471
+ diags.push({
472
+ level: 'error',
473
+ code: 'INTSVC_METHOD_UNKNOWN',
474
+ message: `"${n.id}" (Intsvc.ActivityExecution) has method="${method}"; the activity catalog's MethodName is one of ` +
475
+ `${[...INTSVC_METHODS].join(', ')}` +
476
+ (INTSVC_METHODS.has(method.trim().toUpperCase()) ? ` — write it in upper case: "${method.trim().toUpperCase()}".` : `. Read it from \`uip is activities list ${key} --output json\` (MethodName).`),
477
+ where,
478
+ });
479
+ }
480
+ // 3. `operation` is the catalog activity's Name (or the CRUD verb hand-written exports use): an identifier.
481
+ const operation = text(ctx['operation']);
482
+ if (operation !== undefined && operation.trim() !== '' && !expr(operation) && !/^n\/a$/i.test(operation.trim()) && !/^(<|&lt;)/.test(operation.trim())
483
+ && !/^[A-Za-z][A-Za-z0-9_]*$/.test(operation.trim())) {
484
+ diags.push({
485
+ level: 'error',
486
+ code: 'INTSVC_OPERATION_NOT_IDENTIFIER',
487
+ message: `"${n.id}" (Intsvc.ActivityExecution) has operation="${operation}"; the registry defines it as the activity catalog's Name ` +
488
+ `(\`uip is activities list ${key}\`: CreateTestSet, QueryEntityRecordsCurated) — an identifier, not a REST slug or a label.`,
489
+ where,
490
+ });
491
+ }
492
+ // 4. `objectName` spelled as a REST path: validates, dispatches to nothing. Only a
493
+ // `/` says so — `::` is in curated ObjectNames too (`v2::webSearch`, airdk).
494
+ const objectName = text(ctx['objectName']);
495
+ if (objectName !== undefined && !expr(objectName) && objectName.includes('/')) {
496
+ diags.push({
497
+ level: 'warning',
498
+ code: 'INTSVC_OBJECT_NAME_SLUG',
499
+ message: `"${n.id}" (Intsvc.ActivityExecution) has objectName="${objectName}", which reads as a REST path, not an object. The platform matches ` +
500
+ `on the catalog's ObjectName (\`uip is activities list ${key}\`: TestSet, UploadAttachment, v2::webSearch); a path validates and then dispatches to nothing.`,
501
+ where,
502
+ });
503
+ }
504
+ // 5. The path's placeholders each need a `target: 'path'` input row of that name.
505
+ const path = text(ctx['path']);
506
+ if (path !== undefined && !expr(path)) {
507
+ if (!path.startsWith('/')) {
508
+ diags.push({
509
+ level: 'warning',
510
+ code: 'INTSVC_PATH_NOT_ABSOLUTE',
511
+ message: `"${n.id}" (Intsvc.ActivityExecution) has path="${path}"; the request path \`is resources describe\` returns starts with "/" (e.g. "/v2/{ProjectId}/testsets").`,
512
+ where,
513
+ });
514
+ }
515
+ const params = [...path.matchAll(/\{([^{}]+)\}/g)].map((m) => m[1]);
516
+ if (params.length > 0) {
517
+ // Bound by a row of that name targeting the path, or by a path AGGREGATE —
518
+ // `inputs: { pathParameters: { ProjectId } }` emits one `target="pathParameters"`
519
+ // row holding every placeholder (an expression-valued aggregate binds them all).
520
+ const PATH_TARGETS = new Set(['path', 'pathvariable', 'pathparameters']);
521
+ const bound = new Set();
522
+ let aggregateExpression = false;
523
+ const aggregate = (value) => {
524
+ if (expr(value))
525
+ aggregateExpression = true;
526
+ else if (value && typeof value === 'object')
527
+ for (const k of Object.keys(value))
528
+ bound.add(k);
529
+ };
530
+ for (const r of n.inputRows ?? []) {
531
+ const target = (r.target ?? '').toLowerCase();
532
+ if (target === 'pathparameters' || target === 'pathvariable' || PATH_TARGETS.has(r.name.toLowerCase()))
533
+ aggregate(r.body ?? r.value);
534
+ if (PATH_TARGETS.has(target))
535
+ bound.add(r.name);
536
+ }
537
+ if (!n.inputRows)
538
+ for (const key of ['pathParameters', 'pathVariable'])
539
+ if (key in (n.inputs ?? {}))
540
+ aggregate(n.inputs[key]);
541
+ const unbound = aggregateExpression ? [] : params.filter((p) => !bound.has(p));
542
+ if (unbound.length > 0) {
543
+ diags.push({
544
+ level: 'warning',
545
+ code: 'INTSVC_PATH_PARAM_UNBOUND',
546
+ message: `"${n.id}" (Intsvc.ActivityExecution) path "${path}" has placeholder(s) ${unbound.map((p) => `{${p}}`).join(', ')} with no input row ` +
547
+ `of that name targeting the path. Write \`inputs: { pathParameters: { ${unbound[0]}: '…' } }\` (the other fields stay ` +
548
+ `beside it), or an \`inputRows\` row \`{ name: '${unbound[0]}', type: 'string', value: '…', target: 'path' }\` — a bare ` +
549
+ `\`inputs: { ${unbound[0]} }\` lands in the body and leaves the placeholder unfilled.`,
550
+ where,
551
+ });
552
+ }
553
+ }
554
+ }
555
+ // 6. The connection and folder rows: binding references to declared Connection bindings.
556
+ const bindingRow = (field, attribute) => {
557
+ const v = text(ctx[field]);
558
+ if (v === undefined || v.trim() === '')
559
+ return;
560
+ if (!v.startsWith('=bindings.')) {
561
+ // A literal gets the binding to declare with that value; any other
562
+ // expression (`=vars.connId`) is told what refresh needs instead — a
563
+ // connection is resolved from a root binding, never computed at run time.
564
+ const label = field === 'connection' ? 'conn' : 'folder';
565
+ const refusal = `\`uip maestro bpmn refresh\` refuses it with \`${field} must reference a root binding as "=bindings.<id>"\``;
566
+ diags.push({
567
+ level: 'error',
568
+ code: 'INTSVC_CONNECTION_NOT_BINDING',
569
+ message: expr(v)
570
+ ? `"${n.id}" (Intsvc.ActivityExecution) has ${field}="${v}", an expression; ${refusal}. A ${field} is resolved from a root binding, ` +
571
+ `not computed: declare \`.binding('${label}', { resource: 'Connection', propertyAttribute: '${attribute}', value: '<${field === 'connection' ? 'connection id' : 'folder key'}>' })\` ` +
572
+ `and write ${field}: '=bindings.${label}'.`
573
+ : `"${n.id}" (Intsvc.ActivityExecution) has ${field}="${v}", a literal; ${refusal}. Declare ` +
574
+ `\`.binding('${label}', { resource: 'Connection', propertyAttribute: '${attribute}', value: '${v}' })\` ` +
575
+ `and write ${field}: '=bindings.${label}'.`,
576
+ where,
577
+ });
578
+ return;
579
+ }
580
+ const id = v.slice('=bindings.'.length);
581
+ const b = declared.get(id);
582
+ if (!b) {
583
+ diags.push({
584
+ level: 'error',
585
+ code: 'INTSVC_BINDING_UNDECLARED',
586
+ message: `"${n.id}" (Intsvc.ActivityExecution) reads ${field}="${v}" but the process declares no binding "${id}" ` +
587
+ `(\`uip maestro bpmn validate\` reports MISSING_BINDING). Declare it at the root: ` +
588
+ `\`.binding('${id}', { resource: 'Connection', propertyAttribute: '${attribute}', value: '<${field === 'connection' ? 'connection id' : 'folder key'}>' })\`` +
589
+ (field === 'folderKey' ? ' — keyed by its connection; the builder pairs it with the connection this task reads.' : '.'),
590
+ where,
591
+ });
592
+ return;
593
+ }
594
+ const attr = b.propertyAttribute;
595
+ const resource = b.resource;
596
+ if (attr.toLowerCase() !== attribute.toLowerCase() || resource.toLowerCase() !== 'connection') {
597
+ diags.push({
598
+ level: 'error',
599
+ code: 'INTSVC_BINDING_MISMATCH',
600
+ message: `"${n.id}" (Intsvc.ActivityExecution) reads ${field}="${v}", but binding "${id}" is resource "${resource}" / ` +
601
+ `propertyAttribute "${attr}"; the ${field} row needs resource "Connection" / propertyAttribute "${attribute}".`,
602
+ where,
603
+ });
604
+ }
605
+ };
606
+ bindingRow('connection', 'ConnectionId');
607
+ bindingRow('folderKey', 'folderKey');
608
+ if (text(ctx['connection']) === undefined) {
609
+ diags.push({
610
+ level: 'warning',
611
+ code: 'INTSVC_CONNECTION_MISSING',
612
+ message: `"${n.id}" (Intsvc.ActivityExecution) names no connection. The registry says to omit \`connection\` and \`folderKey\` only ` +
613
+ `when the activity uses none; a connector activity almost always does. Declare a Connection binding and read it as ` +
614
+ `connection: '=bindings.<id>' (\`uip is connections list ${key} --all-folders --output json\` lists the ids).`,
615
+ where,
616
+ });
617
+ }
618
+ }
619
+ }
368
620
  /**
369
621
  * Every `id` in the document must be unique — including across KINDS.
370
622
  *
@@ -0,0 +1,32 @@
1
+ import type { Library } from '../core/library.js';
2
+ import type { ConnectorLiftSummary } from './decompile.js';
3
+ type Moddle = any;
4
+ /** The outcome of verifying one decompile's lifted connector tasks. */
5
+ export interface ConnectorLiftVerdict {
6
+ /** Lifted elements whose recompiled task differs beyond the regenerated fields, with what differed. */
7
+ failed: Map<string, string>;
8
+ }
9
+ /**
10
+ * The part of a connector task that a lift must reproduce, as one comparable
11
+ * string. Row order does not matter (rows are sorted by name); a scalar row's
12
+ * `type` attribute and the response row's schema body do not either.
13
+ */
14
+ export declare function canonicalConnectorTask(el: Moddle): string;
15
+ /**
16
+ * Whether each lifted connector task recompiles to the task the file carried, the
17
+ * regenerated fields aside.
18
+ *
19
+ * Each call is compiled ON ITS OWN — the decompiled preamble (bindings, variables,
20
+ * schemas), a start event, the one `.connector()` line, an end event — rather than
21
+ * the whole process, so a lift the library refuses (a body field it does not carry)
22
+ * is the only one refused, and a process with no other way to compile (a start event
23
+ * missing its connection, say) still gets its connector tasks verified.
24
+ *
25
+ * @param originalXml - The `.bpmn` that was decompiled.
26
+ * @param summary - What the lift did: the ids, the calls, and the preamble to compile them under.
27
+ * @param sdkEntry - Absolute path of the `bpmn-sdk.js` to build against.
28
+ * @param library - The connector library the lift named operations through.
29
+ * @returns The lifted elements that did not come back the same, each with the reason.
30
+ */
31
+ export declare function verifyConnectorLifts(originalXml: string, summary: Pick<ConnectorLiftSummary, 'lifted' | 'calls' | 'preamble'>, sdkEntry: string, library: Library): Promise<ConnectorLiftVerdict>;
32
+ export {};
@@ -0,0 +1,220 @@
1
+ /**
2
+ * connector-lift-verify — the check that makes decompile's connector lift safe to write.
3
+ *
4
+ * A connector task written back as `.connector(key, action, inputs, …)` is NOT
5
+ * byte-identical on recompile, by design: the SDK regenerates what the canvas wrote
6
+ * and the file carried — the `metadata` blob (`designTimeMetadata`, `metadataState`,
7
+ * `telemetryData`), the `operation` verb, the `type` of each scalar row, the response
8
+ * row's schema body, `activityConfigurationVersion`. Everything else about the task
9
+ * must come back exactly: the element and its name, every other context row, every
10
+ * payload row's name, target and value, every output row's name, source and variable.
11
+ * This builds the lifted source, compiles it with the same library, and compares each
12
+ * lifted element against the original under exactly that list — so a lift that lost
13
+ * an input, picked the wrong action or renamed a variable is refused, element by
14
+ * element, and `decompile-cli` writes those verbatim with the reason.
15
+ *
16
+ * Same stance as `lift-verify.ts` for `--style nested`: the lift's rules are an
17
+ * argument, this is the measurement.
18
+ */
19
+ import BpmnModdle from 'bpmn-moddle';
20
+ import { createRequire } from 'node:module';
21
+ import { buildSource } from './lift-verify.js';
22
+ import { serialize } from './serialize.js';
23
+ const require = createRequire(import.meta.url);
24
+ const uipathDescriptor = require('./uipath-moddle.v1.json');
25
+ /**
26
+ * Context rows the SDK regenerates from the library on compile; not compared.
27
+ * `activity` is the op identity the SDK writes beside `objectName` (the platform
28
+ * validator requires it, exports omit it) and `operation` the library's verb where
29
+ * the canvas writes the curated name; `metadata` is the canvas blob; the
30
+ * configuration version is #1287's stamp.
31
+ */
32
+ const REGENERATED_CONTEXT = new Set(['metadata', 'operation', 'activity', 'activityConfigurationVersion']);
33
+ function stableJson(value) {
34
+ return JSON.stringify(value, (_key, v) => v && typeof v === 'object' && !Array.isArray(v)
35
+ ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]]))
36
+ : v);
37
+ }
38
+ /** A row body as a comparable string: parsed JSON when it is JSON, the text otherwise. */
39
+ function bodyText(body) {
40
+ if (body === undefined || body === null)
41
+ return undefined;
42
+ const text = String(body);
43
+ try {
44
+ return stableJson(JSON.parse(text));
45
+ }
46
+ catch {
47
+ return text;
48
+ }
49
+ }
50
+ function payloadOf(el) {
51
+ return (el.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Activity');
52
+ }
53
+ /**
54
+ * The part of a connector task that a lift must reproduce, as one comparable
55
+ * string. Row order does not matter (rows are sorted by name); a scalar row's
56
+ * `type` attribute and the response row's schema body do not either.
57
+ */
58
+ export function canonicalConnectorTask(el) {
59
+ const payload = payloadOf(el);
60
+ const context = Object.fromEntries((payload?.context?.input ?? [])
61
+ .filter((r) => !REGENERATED_CONTEXT.has(r.name))
62
+ .map((r) => [r.name, r.body !== undefined ? bodyText(r.body) : (r.value ?? '')]));
63
+ const inputs = (payload?.input ?? [])
64
+ .map((r) => ({ name: r.name, target: r.target ?? null, value: r.body !== undefined ? bodyText(r.body) : String(r.value ?? '') }))
65
+ // `.connector()` always writes the request body row, `{}` when nothing fills it;
66
+ // an export writes none for an operation with no body. The same request either way.
67
+ .filter((r) => !(r.name === 'body' && r.target === 'body' && r.value === '{}'))
68
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)));
69
+ const outputs = (payload?.output ?? [])
70
+ .map((r) => ({ name: r.name, source: r.source ?? null, var: r.var ?? null, custom: r.custom ?? null, target: r.target ?? null }))
71
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)) || String(a.var).localeCompare(String(b.var)));
72
+ // The activity's other extensions — tags, retry, error mapping — and its loop
73
+ // characteristics: options `.connector()` takes, so a lift that lost one is refused.
74
+ const extensions = (el.extensionElements?.values ?? [])
75
+ .filter((x) => x.$type !== 'uipath:Activity' && x.$type !== 'uipath:EntryPointId')
76
+ .map(plain)
77
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
78
+ return stableJson({
79
+ element: el.$type,
80
+ name: el.name ?? null,
81
+ type: payload?.type?.value ?? null,
82
+ skipCondition: payload?.skipCondition ?? null,
83
+ context,
84
+ inputs,
85
+ outputs,
86
+ extensions,
87
+ loop: el.loopCharacteristics ? plain(el.loopCharacteristics) : null,
88
+ });
89
+ }
90
+ /** A moddle element as plain data: its own properties, recursively, without the `$`-prefixed bookkeeping. */
91
+ function plain(value) {
92
+ if (Array.isArray(value))
93
+ return value.map(plain);
94
+ if (value && typeof value === 'object') {
95
+ const out = { $type: value.$type };
96
+ for (const [k, v] of Object.entries(value)) {
97
+ if (k.startsWith('$'))
98
+ continue;
99
+ out[k] = plain(v);
100
+ }
101
+ return out;
102
+ }
103
+ return value;
104
+ }
105
+ /**
106
+ * Every variable id the document declares or a node's output row writes. An isolated
107
+ * call still reads the process's variables in its expressions (`vars.response11[0].Id`),
108
+ * and the build's expression check refuses a read of an undeclared one — so the ones
109
+ * the preamble does not already declare are stubbed into the verification source.
110
+ */
111
+ function declaredVariableIds(root) {
112
+ const ids = new Set();
113
+ const fromExt = (owner) => {
114
+ for (const ext of owner.extensionElements?.values ?? []) {
115
+ if (ext.$type !== 'uipath:Variables')
116
+ continue;
117
+ for (const list of [ext.input, ext.inputOutput, ext.output])
118
+ for (const v of list ?? [])
119
+ if (v.id)
120
+ ids.add(v.id);
121
+ }
122
+ };
123
+ const walk = (container) => {
124
+ fromExt(container);
125
+ for (const el of container.flowElements ?? []) {
126
+ fromExt(el);
127
+ for (const ext of el.extensionElements?.values ?? [])
128
+ for (const row of ext.output ?? [])
129
+ if (row.var)
130
+ ids.add(row.var);
131
+ if (el.$type === 'bpmn:SubProcess')
132
+ walk(el);
133
+ }
134
+ };
135
+ for (const r of root.rootElements ?? []) {
136
+ if (r.$type === 'bpmn:Process')
137
+ walk(r);
138
+ else
139
+ fromExt(r);
140
+ }
141
+ return [...ids];
142
+ }
143
+ function elementsById(root) {
144
+ const out = new Map();
145
+ const walk = (container) => {
146
+ for (const el of container.flowElements ?? []) {
147
+ out.set(el.id, el);
148
+ if (el.$type === 'bpmn:SubProcess')
149
+ walk(el);
150
+ }
151
+ };
152
+ for (const r of root.rootElements ?? [])
153
+ if (r.$type === 'bpmn:Process')
154
+ walk(r);
155
+ return out;
156
+ }
157
+ /**
158
+ * Whether each lifted connector task recompiles to the task the file carried, the
159
+ * regenerated fields aside.
160
+ *
161
+ * Each call is compiled ON ITS OWN — the decompiled preamble (bindings, variables,
162
+ * schemas), a start event, the one `.connector()` line, an end event — rather than
163
+ * the whole process, so a lift the library refuses (a body field it does not carry)
164
+ * is the only one refused, and a process with no other way to compile (a start event
165
+ * missing its connection, say) still gets its connector tasks verified.
166
+ *
167
+ * @param originalXml - The `.bpmn` that was decompiled.
168
+ * @param summary - What the lift did: the ids, the calls, and the preamble to compile them under.
169
+ * @param sdkEntry - Absolute path of the `bpmn-sdk.js` to build against.
170
+ * @param library - The connector library the lift named operations through.
171
+ * @returns The lifted elements that did not come back the same, each with the reason.
172
+ */
173
+ export async function verifyConnectorLifts(originalXml, summary, sdkEntry, library) {
174
+ const failed = new Map();
175
+ if (summary.lifted.length === 0)
176
+ return { failed };
177
+ const moddle = new BpmnModdle({ uipath: uipathDescriptor });
178
+ const original = (await moddle.fromXML(originalXml, { lax: true })).rootElement;
179
+ const before = elementsById(original);
180
+ // Declared by the preamble means a `.var()` call or a `.vars([{ id, … }])` entry — not
181
+ // a mention in `.preserve({ variableOrder })`, which lists derived declarations the
182
+ // preamble does not write.
183
+ const declares = (v) => {
184
+ const q = JSON.stringify(v).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
185
+ return new RegExp(`\\.var\\(${q}|\\{ id: ${q}, (?:name|type|direction)`).test(summary.preamble);
186
+ };
187
+ const stubs = declaredVariableIds(original)
188
+ .filter((v) => !declares(v))
189
+ .map((v) => ` .var(${JSON.stringify(v)}, "object")`)
190
+ .join('\n');
191
+ for (const id of summary.lifted) {
192
+ const call = summary.calls.get(id);
193
+ const a = before.get(id);
194
+ if (!call || !a) {
195
+ failed.set(id, call ? 'not in the original' : 'the lift recorded no call for it');
196
+ continue;
197
+ }
198
+ const source = `${summary.preamble}\n${stubs ? `${stubs}\n` : ''} .startEvent("verify_start")\n ${call}\n .endEvent("verify_end")\n` +
199
+ ` .sequenceFlow("verify_start", ${JSON.stringify(id)})\n .sequenceFlow(${JSON.stringify(id)}, "verify_end")\n .build();\n`;
200
+ let recompiled;
201
+ try {
202
+ recompiled = await serialize(await buildSource(source, sdkEntry), { library });
203
+ }
204
+ catch (e) {
205
+ // The first three lines: the refusal and, for a build error, the diagnostic under it.
206
+ failed.set(id, `the lifted call did not compile: ${e.message.split('\n').slice(0, 3).join(' ').trim()}`);
207
+ continue;
208
+ }
209
+ const b = elementsById((await moddle.fromXML(recompiled, { lax: true })).rootElement).get(id);
210
+ if (!b) {
211
+ failed.set(id, 'not in the recompiled process');
212
+ continue;
213
+ }
214
+ const ca = canonicalConnectorTask(a);
215
+ const cb = canonicalConnectorTask(b);
216
+ if (ca !== cb)
217
+ failed.set(id, `recompiled task differs:\n was ${ca}\n now ${cb}`);
218
+ }
219
+ return { failed };
220
+ }
@@ -33,6 +33,18 @@ export interface ConnectorCtx {
33
33
  library?: Library;
34
34
  bindings?: Bindings;
35
35
  bindingSink: Map<string, Record<string, unknown>>;
36
+ /**
37
+ * The bindings the author declared in source (`.binding()`), by id. A connector
38
+ * naming one of them takes its `resourceKey` and `default` from the declaration,
39
+ * the way it takes them from a bindings.json entry: without this, a `.binding('c',
40
+ * { value: '<guid>' })` paired with `connection: 'c'` wrote `resourceKey="c"` into
41
+ * the task's context — the id where every real export writes the connection's key.
42
+ */
43
+ declaredBindings?: ReadonlyMap<string, {
44
+ name?: string;
45
+ default?: string;
46
+ resourceKey?: string;
47
+ }>;
36
48
  /**
37
49
  * Declared Maestro `type` for a variable id, for typing an author-mapped output
38
50
  * row. Optional so the connector emitter (which writes its own rows) is
@@ -116,6 +116,21 @@ export function createConnectorTask(ctx, n) {
116
116
  },
117
117
  }),
118
118
  }),
119
+ // Extra mapped rows — `outputs: { <var>: '=response.<field>' }`, or the object
120
+ // form naming and typing the row — after the two the library implies, where
121
+ // every export puts the rows an author added in the designer.
122
+ ...Object.entries(n.outputs ?? {}).map(([varId, spec]) => {
123
+ const row = typeof spec === 'string' ? { source: spec } : spec;
124
+ const props = {
125
+ name: row.name ?? varId,
126
+ type: row.type ?? ctx.varTypeOf?.(varId) ?? 'string',
127
+ source: row.source,
128
+ var: varId,
129
+ };
130
+ if (row.custom)
131
+ props.custom = true;
132
+ return m.create('uipath:Output', props);
133
+ }),
119
134
  ],
120
135
  });
121
136
  return m.create('bpmn:SendTask', {
@@ -180,7 +195,7 @@ export function connectorBindingRef(ctx, name, resource, propertyAttribute, disp
180
195
  function bindingRef(ctx, name, resource, propertyAttribute, displayName, companionKey) {
181
196
  if (!name)
182
197
  return undefined;
183
- const entry = ctx.bindings?.entry(name);
198
+ const entry = (ctx.bindings?.entry(name) ?? ctx.declaredBindings?.get(name));
184
199
  const id = entry?.id ?? name;
185
200
  // `default` is the deploy-time VALUE the runtime resolves, not the author's symbolic
186
201
  // name for the binding. An entry that states only a `resourceKey` — which is what a