@uipath/maestro-builder-sdk 5.2.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/LICENSE +21 -0
- package/README.md +322 -0
- package/dist/bindings-v2.d.ts +37 -0
- package/dist/bindings-v2.js +155 -0
- package/dist/bindings.d.ts +6 -0
- package/dist/bindings.js +6 -0
- package/dist/bpmn/bpmn-expr-check.d.ts +41 -0
- package/dist/bpmn/bpmn-expr-check.js +218 -0
- package/dist/bpmn/bpmn-sdk.d.ts +2257 -0
- package/dist/bpmn/bpmn-sdk.js +1476 -0
- package/dist/bpmn/check-cli.d.ts +3 -0
- package/dist/bpmn/check-cli.js +32 -0
- package/dist/bpmn/check.d.ts +25 -0
- package/dist/bpmn/check.js +430 -0
- package/dist/bpmn/compile-cli.d.ts +3 -0
- package/dist/bpmn/compile-cli.js +60 -0
- package/dist/bpmn/connector.d.ts +56 -0
- package/dist/bpmn/connector.js +199 -0
- package/dist/bpmn/decompile-cli.d.ts +8 -0
- package/dist/bpmn/decompile-cli.js +54 -0
- package/dist/bpmn/decompile.d.ts +6 -0
- package/dist/bpmn/decompile.js +1108 -0
- package/dist/bpmn/format-profile.d.ts +27 -0
- package/dist/bpmn/format-profile.js +11 -0
- package/dist/bpmn/intsvc.d.ts +77 -0
- package/dist/bpmn/intsvc.js +155 -0
- package/dist/bpmn/load.d.ts +4 -0
- package/dist/bpmn/load.js +22 -0
- package/dist/bpmn/merge-cli.d.ts +3 -0
- package/dist/bpmn/merge-cli.js +45 -0
- package/dist/bpmn/merge.d.ts +19 -0
- package/dist/bpmn/merge.js +293 -0
- package/dist/bpmn/registry-types.json +1869 -0
- package/dist/bpmn/serialize.d.ts +28 -0
- package/dist/bpmn/serialize.js +757 -0
- package/dist/bpmn/tidy-cli.d.ts +2 -0
- package/dist/bpmn/tidy-cli.js +39 -0
- package/dist/bpmn/typed-node.d.ts +382 -0
- package/dist/bpmn/typed-node.js +551 -0
- package/dist/bpmn/uipath-moddle.v1.json +823 -0
- package/dist/bpmn/xml-ranges.d.ts +45 -0
- package/dist/bpmn/xml-ranges.js +151 -0
- package/dist/case/case-expr-check.d.ts +35 -0
- package/dist/case/case-expr-check.js +85 -0
- package/dist/case/case-sdk.d.ts +1282 -0
- package/dist/case/case-sdk.js +923 -0
- package/dist/case/check-cli.d.ts +3 -0
- package/dist/case/check-cli.js +31 -0
- package/dist/case/check.d.ts +18 -0
- package/dist/case/check.js +456 -0
- package/dist/case/compile-cli.d.ts +3 -0
- package/dist/case/compile-cli.js +291 -0
- package/dist/case/decompile-cli.d.ts +9 -0
- package/dist/case/decompile-cli.js +117 -0
- package/dist/case/decompile.d.ts +187 -0
- package/dist/case/decompile.js +1170 -0
- package/dist/case/entry-points.d.ts +79 -0
- package/dist/case/entry-points.js +118 -0
- package/dist/case/format-profile.d.ts +60 -0
- package/dist/case/format-profile.js +79 -0
- package/dist/case/ids.d.ts +24 -0
- package/dist/case/ids.js +76 -0
- package/dist/case/load.d.ts +12 -0
- package/dist/case/load.js +32 -0
- package/dist/case/preserve.d.ts +133 -0
- package/dist/case/preserve.js +105 -0
- package/dist/case/serialize.d.ts +10 -0
- package/dist/case/serialize.js +1414 -0
- package/dist/check-cli.d.ts +3 -0
- package/dist/check-cli.js +118 -0
- package/dist/check.d.ts +146 -0
- package/dist/check.js +4898 -0
- package/dist/cli/commands/bpmn.d.ts +2 -0
- package/dist/cli/commands/bpmn.js +73 -0
- package/dist/cli/commands/case.d.ts +2 -0
- package/dist/cli/commands/case.js +60 -0
- package/dist/cli/commands/flow.d.ts +2 -0
- package/dist/cli/commands/flow.js +99 -0
- package/dist/cli/commands/registry.d.ts +10 -0
- package/dist/cli/commands/registry.js +170 -0
- package/dist/cli/common.d.ts +30 -0
- package/dist/cli/common.js +92 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli-run.d.ts +2 -0
- package/dist/cli-run.js +42 -0
- package/dist/compile-cli.d.ts +3 -0
- package/dist/compile-cli.js +160 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.js +201 -0
- package/dist/core/actions.d.ts +3244 -0
- package/dist/core/actions.js +1062 -0
- package/dist/core/binding-messages.d.ts +1 -0
- package/dist/core/binding-messages.js +16 -0
- package/dist/core/bindings.d.ts +12 -0
- package/dist/core/bindings.js +67 -0
- package/dist/core/ceql.d.ts +73 -0
- package/dist/core/ceql.js +272 -0
- package/dist/core/cli-spelling.d.ts +39 -0
- package/dist/core/cli-spelling.js +62 -0
- package/dist/core/connector-raw-node.d.ts +21 -0
- package/dist/core/connector-raw-node.js +47 -0
- package/dist/core/connectors.d.ts +184 -0
- package/dist/core/connectors.js +128 -0
- package/dist/core/expr-check.d.ts +93 -0
- package/dist/core/expr-check.js +186 -0
- package/dist/core/expr.d.ts +323 -0
- package/dist/core/expr.js +400 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.js +10 -0
- package/dist/core/library.d.ts +440 -0
- package/dist/core/library.js +1183 -0
- package/dist/core/list-envelope.d.ts +45 -0
- package/dist/core/list-envelope.js +97 -0
- package/dist/core/lookup-store.d.ts +22 -0
- package/dist/core/lookup-store.js +85 -0
- package/dist/core/lookups.d.ts +227 -0
- package/dist/core/lookups.js +231 -0
- package/dist/core/node-classes.d.ts +234 -0
- package/dist/core/node-classes.js +158 -0
- package/dist/core/script-return.d.ts +68 -0
- package/dist/core/script-return.js +197 -0
- package/dist/core/stable-id.d.ts +28 -0
- package/dist/core/stable-id.js +37 -0
- package/dist/core-definitions.json +16249 -0
- package/dist/decompile-cli.d.ts +3 -0
- package/dist/decompile-cli.js +131 -0
- package/dist/decompile.d.ts +138 -0
- package/dist/decompile.js +2482 -0
- package/dist/emit.d.ts +10 -0
- package/dist/emit.js +60 -0
- package/dist/event-filters.d.ts +130 -0
- package/dist/event-filters.js +137 -0
- package/dist/flow-expr-check.d.ts +17 -0
- package/dist/flow-expr-check.js +193 -0
- package/dist/flow-sdk.d.ts +1341 -0
- package/dist/flow-sdk.js +1164 -0
- package/dist/flow-variables.d.ts +13 -0
- package/dist/flow-variables.js +42 -0
- package/dist/format-profile.d.ts +72 -0
- package/dist/format-profile.js +97 -0
- package/dist/generators/_connections.py +287 -0
- package/dist/generators/_events.py +355 -0
- package/dist/generators/_lookups.py +114 -0
- package/dist/generators/_naming.py +185 -0
- package/dist/generators/_objects.py +299 -0
- package/dist/generators/_resolve.py +273 -0
- package/dist/generators/convert_library_to_md.py +1441 -0
- package/dist/generators/extract_library.py +125 -0
- package/dist/generators/generate_connectors_ts.py +936 -0
- package/dist/generators/generate_library_json.py +2527 -0
- package/dist/generators/generate_triggers_ts.py +110 -0
- package/dist/generators/materialize_registry.py +155 -0
- package/dist/generators/prepare_connector.py +1688 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +35 -0
- package/dist/ixp-definition.json +1982 -0
- package/dist/library.d.ts +6 -0
- package/dist/library.js +6 -0
- package/dist/load.d.ts +16 -0
- package/dist/load.js +95 -0
- package/dist/merge-cli.d.ts +3 -0
- package/dist/merge-cli.js +47 -0
- package/dist/merge.d.ts +125 -0
- package/dist/merge.js +0 -0
- package/dist/migrate.d.ts +15 -0
- package/dist/migrate.js +245 -0
- package/dist/node-runtime.d.ts +3 -0
- package/dist/node-runtime.js +22 -0
- package/dist/package-name.d.ts +17 -0
- package/dist/package-name.js +17 -0
- package/dist/ref-resolve.d.ts +77 -0
- package/dist/ref-resolve.js +170 -0
- package/dist/registry/cache.d.ts +55 -0
- package/dist/registry/cache.js +134 -0
- package/dist/registry/curated-overlay.d.ts +16 -0
- package/dist/registry/curated-overlay.js +62 -0
- package/dist/registry/extractor.d.ts +22 -0
- package/dist/registry/extractor.js +18 -0
- package/dist/registry/materialize-bin.d.ts +2 -0
- package/dist/registry/materialize-bin.js +7 -0
- package/dist/registry/materialize-cli.d.ts +1 -0
- package/dist/registry/materialize-cli.js +46 -0
- package/dist/registry/materializer.d.ts +23 -0
- package/dist/registry/materializer.js +34 -0
- package/dist/registry/path-cli.d.ts +1 -0
- package/dist/registry/path-cli.js +56 -0
- package/dist/registry/prepare-cli.d.ts +62 -0
- package/dist/registry/prepare-cli.js +185 -0
- package/dist/registry/pull-cli.d.ts +1 -0
- package/dist/registry/pull-cli.js +274 -0
- package/dist/registry/python.d.ts +26 -0
- package/dist/registry/python.js +46 -0
- package/dist/registry/search-cli.d.ts +52 -0
- package/dist/registry/search-cli.js +159 -0
- package/dist/schema-version.d.ts +165 -0
- package/dist/schema-version.js +148 -0
- package/dist/serialize.d.ts +64 -0
- package/dist/serialize.js +6398 -0
- package/lib/README.md +112 -0
- package/lib/library-json.samples/index.json +158 -0
- package/lib/library-json.samples/invariant-collections.json +83 -0
- package/lib/library-json.samples/list-envelopes.json +123 -0
- package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.json +131 -0
- package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.v1def.json +306 -0
- package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.json +86 -0
- package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.v1def.json +208 -0
- package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.json +2040 -0
- package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.v1def.json +195 -0
- package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.json +169 -0
- package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.v1def.json +209 -0
- package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.json +63 -0
- package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.v1def.json +262 -0
- package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.json +63 -0
- package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.v1def.json +213 -0
- package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.json +227 -0
- package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.v1def.json +373 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.json +116 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.v1def.json +369 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.json +56 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.v1def.json +256 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.json +116 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.v1def.json +320 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.json +56 -0
- package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.v1def.json +207 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.json +107 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.v1def.json +264 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.json +142 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.v1def.json +306 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.json +107 -0
- package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.v1def.json +217 -0
- package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.json +85 -0
- package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.v1def.json +209 -0
- package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.json +755 -0
- package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.v1def.json +513 -0
- package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.json +122 -0
- package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.v1def.json +227 -0
- package/package.json +85 -0
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* case/serialize — turn a BuiltCase (from the builder) into a real UiPath
|
|
3
|
+
* `caseplan.json` (schema V30, as defined by @uipath/case-schema / validated by
|
|
4
|
+
* `uip maestro case validate`).
|
|
5
|
+
*
|
|
6
|
+
* A case is a hierarchy, not a graph: the output has `edges: []` and expresses
|
|
7
|
+
* all flow through Stage `entryConditions`/`exitConditions` and the case-level
|
|
8
|
+
* `metadata.caseExitRules`. Symbolic references in rules (a stage label, a task
|
|
9
|
+
* name) are resolved here to the generated ids.
|
|
10
|
+
*/
|
|
11
|
+
import { IdGen } from './ids.js';
|
|
12
|
+
import { transportHttpMethod } from '../config.js';
|
|
13
|
+
import { refuseLookupToken } from '../core/lookups.js';
|
|
14
|
+
import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
|
|
15
|
+
import { resolveSchemaVersion } from '../schema-version.js';
|
|
16
|
+
import { caseJsonPreservation, caseJsonPreservationErrors, } from './preserve.js';
|
|
17
|
+
import { readEventFilter, eventFilterProblem, eventFilterTreeLeaf } from '../event-filters.js';
|
|
18
|
+
/**
|
|
19
|
+
* Condition `displayName` allocator.
|
|
20
|
+
*
|
|
21
|
+
* Condition names must be unique across the WHOLE case: `uip maestro case
|
|
22
|
+
* validate` rejects a repeat as CASE_MGMT_RULE_NAME_DUPLICATE, at error
|
|
23
|
+
* severity, over a namespace spanning stage entry, stage exit and task entry
|
|
24
|
+
* conditions plus `metadata.caseExitRules`. (The validator does exempt the
|
|
25
|
+
* DESIGNER's own generated names — the `Entry rule <n>` / `Exit rule <n>` /
|
|
26
|
+
* `Completion rule <n>` families and a few singletons — which is exactly the
|
|
27
|
+
* allowlist the old constants missed: `Entry rule` was one token short of it and
|
|
28
|
+
* `Complete rule` was never in it. `check` mirrors that list; emitting genuinely
|
|
29
|
+
* unique names here means this side does not depend on it.)
|
|
30
|
+
*
|
|
31
|
+
* So a generated default is derived from the condition's owner rather than being
|
|
32
|
+
* a constant — with constants, a single stage was already enough to fail,
|
|
33
|
+
* because the stage's own entry rule and its task's entry rule both defaulted to
|
|
34
|
+
* "Entry rule".
|
|
35
|
+
*
|
|
36
|
+
* Authored names are reserved FIRST, so a generated default never steals a name
|
|
37
|
+
* the author spells out later in the chain, and are then returned untouched —
|
|
38
|
+
* renaming what an author wrote would be the worse surprise. A collision among
|
|
39
|
+
* authored names is left alone here and reported by `case check` (DUP_RULE_NAME).
|
|
40
|
+
*/
|
|
41
|
+
function conditionNamer(built) {
|
|
42
|
+
const taken = new Set();
|
|
43
|
+
const reserve = (name) => void (name && taken.add(name));
|
|
44
|
+
for (const c of built.caseExit)
|
|
45
|
+
reserve(c.displayName);
|
|
46
|
+
for (const stage of built.stages) {
|
|
47
|
+
for (const c of stage.entry)
|
|
48
|
+
reserve(c.displayName);
|
|
49
|
+
for (const c of stage.exit)
|
|
50
|
+
reserve(c.displayName);
|
|
51
|
+
for (const lane of stage.lanes)
|
|
52
|
+
for (const task of lane)
|
|
53
|
+
for (const c of task.entry)
|
|
54
|
+
reserve(c.displayName);
|
|
55
|
+
}
|
|
56
|
+
return (authored, fallback) => {
|
|
57
|
+
if (authored)
|
|
58
|
+
return authored;
|
|
59
|
+
let name = fallback;
|
|
60
|
+
for (let n = 2; taken.has(name); n += 1)
|
|
61
|
+
name = `${fallback} ${n}`;
|
|
62
|
+
taken.add(name);
|
|
63
|
+
return name;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function serialize(built, opts = {}) {
|
|
67
|
+
const ids = new IdGen();
|
|
68
|
+
const caseId = ids.caseId(built.id);
|
|
69
|
+
// ── First pass: allocate stage + task ids, build resolution maps ──
|
|
70
|
+
const resolver = {
|
|
71
|
+
stageIdByLabel: new Map(),
|
|
72
|
+
taskIdByKey: new Map(),
|
|
73
|
+
library: opts.library,
|
|
74
|
+
bindings: opts.bindings,
|
|
75
|
+
bindingSink: new Map(),
|
|
76
|
+
varCompanions: new Map(),
|
|
77
|
+
connectorOutputCounts: new Map(),
|
|
78
|
+
slaId: new Map(),
|
|
79
|
+
slaIdByName: new Map(),
|
|
80
|
+
escalationId: new Map(),
|
|
81
|
+
escalationIdByName: new Map(),
|
|
82
|
+
conditionName: conditionNamer(built),
|
|
83
|
+
};
|
|
84
|
+
// Preallocate SLA + escalation ids (before any rule is emitted) so a
|
|
85
|
+
// `sla-status-change` rule can resolve the `slaId`/`escalationId` it points at.
|
|
86
|
+
// Ids are deterministic (seeded on displayName when present, else position).
|
|
87
|
+
const prealloc = (slas, scope) => {
|
|
88
|
+
slas.forEach((sla, i) => {
|
|
89
|
+
const seed = sla.displayName ? `sla:${sla.displayName}` : `sla:${scope}:${i}`;
|
|
90
|
+
const id = ids.make('sla_', 8, seed);
|
|
91
|
+
resolver.slaId.set(sla, id);
|
|
92
|
+
if (sla.displayName)
|
|
93
|
+
resolver.slaIdByName.set(sla.displayName, id);
|
|
94
|
+
sla.escalations.forEach((e, j) => {
|
|
95
|
+
const eseed = e.displayName ? `esc:${e.displayName}` : `esc:${scope}:${i}:${j}`;
|
|
96
|
+
const eid = ids.make('esc_', 6, eseed);
|
|
97
|
+
resolver.escalationId.set(e, eid);
|
|
98
|
+
if (e.displayName)
|
|
99
|
+
resolver.escalationIdByName.set(e.displayName, eid);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
prealloc(built.slaRules, 'case');
|
|
104
|
+
built.stages.forEach((stage) => prealloc(stage.slaRules, stage.label));
|
|
105
|
+
const stageIds = [];
|
|
106
|
+
const taskIds = []; // [stageIndex][lane][taskIndex]
|
|
107
|
+
// Task displayNames must be unique across the whole case: the platform rejects
|
|
108
|
+
// duplicates (CASE_MGMT_STAGE_TASK_NAME_DUPLICATE, CRITICAL), and a bare
|
|
109
|
+
// `selected-tasks-completed` reference resolves against a single name→id map, so
|
|
110
|
+
// a duplicate would silently mis-resolve to the first task. Fail fast instead.
|
|
111
|
+
const seenTaskNames = new Set();
|
|
112
|
+
built.stages.forEach((stage, si) => {
|
|
113
|
+
const sid = ids.stageId(`stage:${stage.label}`);
|
|
114
|
+
if (resolver.stageIdByLabel.has(stage.label))
|
|
115
|
+
throw new Error(`duplicate stage label "${stage.label}".`);
|
|
116
|
+
resolver.stageIdByLabel.set(stage.label, sid);
|
|
117
|
+
stageIds.push(sid);
|
|
118
|
+
taskIds.push([]);
|
|
119
|
+
stage.lanes.forEach((lane, li) => {
|
|
120
|
+
taskIds[si].push([]);
|
|
121
|
+
lane.forEach((task) => {
|
|
122
|
+
if (seenTaskNames.has(task.name))
|
|
123
|
+
throw new Error(`duplicate task displayName "${task.name}" — task names must be unique across the case.`);
|
|
124
|
+
seenTaskNames.add(task.name);
|
|
125
|
+
const tid = ids.taskId(`task:${stage.label}/${task.name}`);
|
|
126
|
+
taskIds[si][li].push(tid);
|
|
127
|
+
// Register both the qualified `Stage/Task` key and the (now unique) bare name.
|
|
128
|
+
resolver.taskIdByKey.set(`${stage.label}/${task.name}`, tid);
|
|
129
|
+
resolver.taskIdByKey.set(task.name, tid);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// ── Second pass: emit nodes ──
|
|
134
|
+
const nodes = [];
|
|
135
|
+
const triggerNodes = emitTriggers(built.triggers, ids, resolver);
|
|
136
|
+
nodes.push(...triggerNodes);
|
|
137
|
+
// Map each declared trigger to its emitted node (by object identity) so a
|
|
138
|
+
// trigger-bound In-arg can resolve `{ from }` to the node it wires onto.
|
|
139
|
+
const triggerNodeByRef = new Map();
|
|
140
|
+
built.triggers.forEach((t, i) => triggerNodeByRef.set(t, triggerNodes[i]));
|
|
141
|
+
built.stages.forEach((stage, si) => {
|
|
142
|
+
nodes.push(emitStage(stage, si, stageIds[si], taskIds[si], ids, resolver));
|
|
143
|
+
});
|
|
144
|
+
// ── Metadata (case-level) ──
|
|
145
|
+
const metadata = {
|
|
146
|
+
caseIdentifier: built.identifier,
|
|
147
|
+
caseAppEnabled: built.caseAppEnabled,
|
|
148
|
+
caseIdentifierType: built.identifierType,
|
|
149
|
+
publishVersion: CASE_FORMAT_PROFILE.publishVersion,
|
|
150
|
+
};
|
|
151
|
+
if (built.caseAppConfig) {
|
|
152
|
+
metadata.caseAppConfig = {
|
|
153
|
+
caseSummary: built.caseAppConfig.summary,
|
|
154
|
+
sections: built.caseAppConfig.sections.map((section, index) => ({
|
|
155
|
+
id: section.id ?? ids.make('section-', 6, `case-app:${index}:${section.title}`),
|
|
156
|
+
title: section.title,
|
|
157
|
+
details: JSON.stringify(section.details),
|
|
158
|
+
})),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (built.allowAdhocOptionalStageTasks !== undefined) {
|
|
162
|
+
metadata.allowAdhocOptionalStageTasks = built.allowAdhocOptionalStageTasks;
|
|
163
|
+
}
|
|
164
|
+
const selectNextStage = built.stages.flatMap((stage) => stage.exit)
|
|
165
|
+
.filter((exit) => exit.selectNextStage !== undefined);
|
|
166
|
+
if (selectNextStage.length > 1)
|
|
167
|
+
throw new Error('select-next-stage can be configured on at most one stage exit per case.');
|
|
168
|
+
if (selectNextStage[0]?.selectNextStage) {
|
|
169
|
+
const { objectName, connector } = selectNextStage[0].selectNextStage;
|
|
170
|
+
if (selectNextStage[0].type !== 'wait-for-user')
|
|
171
|
+
throw new Error('select-next-stage requires an exit with type "wait-for-user".');
|
|
172
|
+
if (!objectName.trim())
|
|
173
|
+
throw new Error('select-next-stage objectName must be non-empty.');
|
|
174
|
+
const context = [];
|
|
175
|
+
if (connector) {
|
|
176
|
+
context.push(io('connectorKey', 'string', { value: connector.connectorKey }));
|
|
177
|
+
const connection = bindingRef(resolver, connector.connection, 'Connection', 'ConnectionId', `${connector.connectorKey} connection`);
|
|
178
|
+
if (connection) {
|
|
179
|
+
context.push(io('connection', 'string', { value: `=bindings.${connection.id}` }));
|
|
180
|
+
context.push(io('resourceKey', 'string', { value: connection.resourceKey }));
|
|
181
|
+
}
|
|
182
|
+
const folder = bindingRef(resolver, connector.folder, 'Connection', 'folderKey', 'FolderKey');
|
|
183
|
+
if (folder)
|
|
184
|
+
context.push(io('folderKey', 'string', { value: `=bindings.${folder.id}` }));
|
|
185
|
+
}
|
|
186
|
+
context.push(io('objectName', 'string', { value: objectName }));
|
|
187
|
+
if (connector)
|
|
188
|
+
context.push(io('operation', 'string', { value: connector.operation }));
|
|
189
|
+
metadata.waitForHumanSelectNextStageDFConnector = {
|
|
190
|
+
id: 'SelectNextStageConnector',
|
|
191
|
+
elementId: 'root-SelectNextStageConnector',
|
|
192
|
+
type: 'wait-for-connector',
|
|
193
|
+
displayName: 'Select next stage',
|
|
194
|
+
shouldRunOnlyOnce: false,
|
|
195
|
+
data: {
|
|
196
|
+
serviceType: CASE_FORMAT_PROFILE.waitConnectorServiceType,
|
|
197
|
+
context,
|
|
198
|
+
inputs: [],
|
|
199
|
+
outputs: [],
|
|
200
|
+
bindings: [],
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (built.caseExit.length) {
|
|
205
|
+
metadata.caseExitRules = built.caseExit.map((c, i) => emitCaseExit(c, i, ids, resolver));
|
|
206
|
+
}
|
|
207
|
+
if (built.slaRules.length) {
|
|
208
|
+
metadata.slaRules = built.slaRules.map((s, i, all) => emitSla(s, i, all.length, ids, resolver));
|
|
209
|
+
}
|
|
210
|
+
// Variables: declared vars + the readable companions that task outputs bound
|
|
211
|
+
// (collected during stage emission above). A declared var wins on a name clash.
|
|
212
|
+
const variables = emitVariables(built.vars, ids, triggerNodeByRef);
|
|
213
|
+
const inputOutputs = variables.inputOutputs;
|
|
214
|
+
const declared = new Set(inputOutputs.map((e) => e.id));
|
|
215
|
+
for (const [name, companion] of resolver.varCompanions)
|
|
216
|
+
if (!declared.has(name))
|
|
217
|
+
inputOutputs.push(companion);
|
|
218
|
+
const result = {
|
|
219
|
+
id: caseId,
|
|
220
|
+
name: built.name,
|
|
221
|
+
// Optional case description at the document root (schema-valid; the skill
|
|
222
|
+
// emits it too). Only written when set, so an undescribed case omits the key.
|
|
223
|
+
...(built.description ? { description: built.description } : {}),
|
|
224
|
+
// The declared grammar version: `.schemaVersion()`'s when the author set
|
|
225
|
+
// one, else the profile's write version. Resolved here as well as in
|
|
226
|
+
// `check()` so a skipped static pass cannot stamp a version this build
|
|
227
|
+
// cannot back.
|
|
228
|
+
version: resolveSchemaVersion(CASE_SCHEMA_VERSIONS, built.version),
|
|
229
|
+
bindings: [...resolver.bindingSink.values()],
|
|
230
|
+
variables,
|
|
231
|
+
metadata,
|
|
232
|
+
nodes,
|
|
233
|
+
edges: [],
|
|
234
|
+
...(CASE_FORMAT_PROFILE.layoutPolicy === 'sidecar'
|
|
235
|
+
? {
|
|
236
|
+
layout: {
|
|
237
|
+
nodes: Object.fromEntries(stageIds.map((id, index) => [id, { position: { x: 100 + index * 500, y: 200 } }])),
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
: {}),
|
|
241
|
+
};
|
|
242
|
+
return applyAuthorLayout(applyPreservation(result, caseJsonPreservation(built)), built.layout);
|
|
243
|
+
}
|
|
244
|
+
const CASE_NODE_LAYOUT_FIELDS = ['position', 'style', 'measured', 'width', 'height', 'zIndex'];
|
|
245
|
+
/**
|
|
246
|
+
* Resolve author-level layout names only after preservation has restored foreign
|
|
247
|
+
* node identities. Authored fields then win field-by-field on the named entry;
|
|
248
|
+
* untouched preserved nodes, edges, fields, and ordering remain as received.
|
|
249
|
+
*/
|
|
250
|
+
function applyAuthorLayout(result, authored) {
|
|
251
|
+
const entries = [
|
|
252
|
+
...Object.entries(authored?.stages ?? {}).map(([name, layout]) => ({ kind: 'stage', name, layout })),
|
|
253
|
+
...Object.entries(authored?.triggers ?? {}).map(([name, layout]) => ({ kind: 'trigger', name, layout })),
|
|
254
|
+
];
|
|
255
|
+
if (!entries.length)
|
|
256
|
+
return result;
|
|
257
|
+
const nodes = result.nodes;
|
|
258
|
+
const layoutValue = result.layout;
|
|
259
|
+
if (layoutValue !== undefined && (!layoutValue || typeof layoutValue !== 'object' || Array.isArray(layoutValue)))
|
|
260
|
+
throw new Error('case layout: preserved layout must be an object before authored entries can be applied.');
|
|
261
|
+
const existingLayout = (layoutValue ?? {});
|
|
262
|
+
const existingNodes = existingLayout.nodes;
|
|
263
|
+
if (existingNodes !== undefined && (!existingNodes || typeof existingNodes !== 'object' || Array.isArray(existingNodes)))
|
|
264
|
+
throw new Error('case layout: preserved layout.nodes must be an object before authored entries can be applied.');
|
|
265
|
+
const layoutNodes = { ...(existingNodes ?? {}) };
|
|
266
|
+
const layout = { ...existingLayout, nodes: layoutNodes };
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
const matches = nodes.filter((node) => {
|
|
269
|
+
if (entry.kind === 'stage')
|
|
270
|
+
return String(node.type ?? '').endsWith('Stage') && node.data?.label === entry.name;
|
|
271
|
+
const trigger = node.type === 'case-management:Trigger' || node.type === 'uipath.case.trigger';
|
|
272
|
+
const label = node.data?.display?.label ?? node.data?.label;
|
|
273
|
+
return trigger && label === entry.name;
|
|
274
|
+
});
|
|
275
|
+
if (matches.length !== 1)
|
|
276
|
+
throw new Error(`case layout: ${entry.kind} "${entry.name}" matches ${matches.length} emitted nodes; expected exactly 1.`);
|
|
277
|
+
const nodeId = matches[0].id;
|
|
278
|
+
if (typeof nodeId !== 'string' || !nodeId)
|
|
279
|
+
throw new Error(`case layout: ${entry.kind} "${entry.name}" has no emitted node id.`);
|
|
280
|
+
const unknown = Object.keys(entry.layout).filter((field) => !CASE_NODE_LAYOUT_FIELDS.includes(field));
|
|
281
|
+
if (unknown.length)
|
|
282
|
+
throw new Error(`case layout: ${entry.kind} "${entry.name}" has unsupported field "${unknown[0]}".`);
|
|
283
|
+
const previous = layoutNodes[nodeId];
|
|
284
|
+
if (previous !== undefined && (!previous || typeof previous !== 'object' || Array.isArray(previous)))
|
|
285
|
+
throw new Error(`case layout: preserved entry for ${entry.kind} "${entry.name}" must be an object.`);
|
|
286
|
+
const merged = { ...(previous ?? {}) };
|
|
287
|
+
for (const field of CASE_NODE_LAYOUT_FIELDS)
|
|
288
|
+
if (entry.layout[field] !== undefined)
|
|
289
|
+
merged[field] = entry.layout[field];
|
|
290
|
+
if (Object.keys(merged).length)
|
|
291
|
+
layoutNodes[nodeId] = merged;
|
|
292
|
+
}
|
|
293
|
+
if (Object.keys(layoutNodes).length)
|
|
294
|
+
result.layout = layout;
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
function applyPreservation(result, preservation) {
|
|
298
|
+
if (!preservation)
|
|
299
|
+
return result;
|
|
300
|
+
const errors = caseJsonPreservationErrors(preservation);
|
|
301
|
+
if (errors.length)
|
|
302
|
+
throw new Error(`invalid Case JSON preservation payload: ${errors.join('; ')}`);
|
|
303
|
+
applyPreservedIdentities(result, preservation);
|
|
304
|
+
Object.assign(result, preservation.topLevel ?? {});
|
|
305
|
+
Object.assign(result.metadata, preservation.metadata ?? {});
|
|
306
|
+
const caseAppConfig = result.metadata.caseAppConfig;
|
|
307
|
+
if (caseAppConfig && preservation.caseAppConfigFields) {
|
|
308
|
+
for (const [field, value] of Object.entries(preservation.caseAppConfigFields.fields ?? {})) {
|
|
309
|
+
if (Object.prototype.hasOwnProperty.call(caseAppConfig, field))
|
|
310
|
+
throw new Error(`preserved caseAppConfig field "${field}" collides with serializer-owned output.`);
|
|
311
|
+
caseAppConfig[field] = value;
|
|
312
|
+
}
|
|
313
|
+
for (const entry of preservation.caseAppConfigFields.sections ?? []) {
|
|
314
|
+
const section = caseAppConfig.sections?.find((candidate) => candidate.id === entry.id);
|
|
315
|
+
if (!section)
|
|
316
|
+
continue;
|
|
317
|
+
for (const [field, value] of Object.entries(entry.fields)) {
|
|
318
|
+
if (Object.prototype.hasOwnProperty.call(section, field))
|
|
319
|
+
throw new Error(`preserved Case App section "${entry.id}" field "${field}" collides with serializer-owned output.`);
|
|
320
|
+
section[field] = value;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const variables = result.variables;
|
|
325
|
+
for (const [lane, order] of Object.entries(preservation.variableOrder ?? {})) {
|
|
326
|
+
const rows = variables[lane] ?? [];
|
|
327
|
+
const byName = new Map(rows.map((row) => [String(row.name ?? ''), row]));
|
|
328
|
+
const ordered = (order ?? []).flatMap((name) => byName.get(name) ? [byName.get(name)] : []);
|
|
329
|
+
const named = new Set(order ?? []);
|
|
330
|
+
variables[lane] = [...ordered, ...rows.filter((row) => !named.has(String(row.name ?? '')))];
|
|
331
|
+
}
|
|
332
|
+
for (const [lane, byName] of Object.entries(preservation.variableFields ?? {})) {
|
|
333
|
+
const rows = variables[lane] ?? [];
|
|
334
|
+
for (const [name, fields] of Object.entries(byName ?? {})) {
|
|
335
|
+
const row = rows.find((candidate) => candidate.name === name);
|
|
336
|
+
if (!row)
|
|
337
|
+
throw new Error(`preserved variable "${name}" was not regenerated in variables.${lane}.`);
|
|
338
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
339
|
+
if (Object.prototype.hasOwnProperty.call(row, field))
|
|
340
|
+
throw new Error(`preserved variable "${name}" field "${field}" collides with serializer-owned output.`);
|
|
341
|
+
row[field] = value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
for (const [lane, byName] of Object.entries(preservation.legacyVariableDefaults ?? {})) {
|
|
346
|
+
const rows = variables[lane] ?? [];
|
|
347
|
+
for (const [name, value] of Object.entries(byName ?? {})) {
|
|
348
|
+
const row = rows.find((candidate) => candidate.name === name);
|
|
349
|
+
if (!row)
|
|
350
|
+
throw new Error(`legacy variable default "${name}" was not regenerated in variables.${lane}.`);
|
|
351
|
+
const encoded = JSON.stringify(value);
|
|
352
|
+
if (row.default !== encoded)
|
|
353
|
+
throw new Error(`legacy variable default "${name}" does not match the serializer-owned default.`);
|
|
354
|
+
row.default = value;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const stageNodes = result.nodes
|
|
358
|
+
.filter((node) => String(node.type ?? '').endsWith('Stage'));
|
|
359
|
+
for (const entry of preservation.taskFields ?? []) {
|
|
360
|
+
const stage = stageNodes.find((candidate) => candidate.data?.label === entry.stage);
|
|
361
|
+
const task = stage?.data?.tasks?.[entry.lane]?.find((candidate) => candidate.displayName === entry.task);
|
|
362
|
+
// A brownfield edit may deliberately delete a typed task or its stage. Its
|
|
363
|
+
// attached designer-only fields leave with that construct; surviving tasks
|
|
364
|
+
// still collide-refuse below instead of letting preservation overwrite
|
|
365
|
+
// serializer-owned output.
|
|
366
|
+
if (!task)
|
|
367
|
+
continue;
|
|
368
|
+
for (const [field, value] of Object.entries(entry.fields)) {
|
|
369
|
+
if (Object.prototype.hasOwnProperty.call(task, field)) {
|
|
370
|
+
// Agreement is not a collision. `elementId` moved from the preservation
|
|
371
|
+
// channel to serializer-owned, so a source generated by an older
|
|
372
|
+
// `case decompile` still carries it in its payload — and it reconstructs
|
|
373
|
+
// to the same `<stageId>-<taskId>`, because preservation pins the ids it
|
|
374
|
+
// is built from. Skipping the identical write keeps those sources
|
|
375
|
+
// compiling. A genuine disagreement still throws rather than letting the
|
|
376
|
+
// channel quietly overwrite typed output.
|
|
377
|
+
if (task[field] === value)
|
|
378
|
+
continue;
|
|
379
|
+
throw new Error(`preserved task "${entry.stage}/${entry.task}" field "${field}" collides with serializer-owned output.`);
|
|
380
|
+
}
|
|
381
|
+
task[field] = value;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const entry of preservation.omitTaskElementIds ?? []) {
|
|
385
|
+
const stage = stageNodes.find((candidate) => candidate.data?.label === entry.stage);
|
|
386
|
+
const task = stage?.data?.tasks?.[entry.lane]?.find((candidate) => candidate.displayName === entry.task);
|
|
387
|
+
if (task)
|
|
388
|
+
delete task.elementId;
|
|
389
|
+
}
|
|
390
|
+
for (const label of preservation.omitStagePositions ?? []) {
|
|
391
|
+
const stage = stageNodes.find((candidate) => candidate.data?.label === label);
|
|
392
|
+
if (!stage)
|
|
393
|
+
continue;
|
|
394
|
+
delete stage.position;
|
|
395
|
+
}
|
|
396
|
+
const triggerNodes = result.nodes
|
|
397
|
+
.filter((node) => node.type === 'case-management:Trigger' || node.type === 'uipath.case.trigger');
|
|
398
|
+
for (const [index, fields] of (preservation.triggerDataFields ?? []).entries()) {
|
|
399
|
+
const trigger = triggerNodes[index];
|
|
400
|
+
if (!trigger)
|
|
401
|
+
throw new Error(`trigger ${index + 1} was not regenerated for preserved data fields.`);
|
|
402
|
+
Object.assign(trigger.data, fields);
|
|
403
|
+
}
|
|
404
|
+
if (preservation.nodes?.length) {
|
|
405
|
+
const nodes = result.nodes;
|
|
406
|
+
for (const entry of [...preservation.nodes].sort((a, b) => a.index - b.index))
|
|
407
|
+
nodes.splice(Math.min(entry.index, nodes.length), 0, entry.node);
|
|
408
|
+
}
|
|
409
|
+
if (preservation.nodeOrder?.length) {
|
|
410
|
+
const nodes = result.nodes;
|
|
411
|
+
const order = new Map(preservation.nodeOrder.map((id, index) => [id, index]));
|
|
412
|
+
nodes.sort((left, right) => (order.get(String(left.id ?? '')) ?? Number.MAX_SAFE_INTEGER)
|
|
413
|
+
- (order.get(String(right.id ?? '')) ?? Number.MAX_SAFE_INTEGER));
|
|
414
|
+
}
|
|
415
|
+
if (preservation.edges !== undefined)
|
|
416
|
+
result.edges = preservation.edges;
|
|
417
|
+
if (preservation.layout !== undefined)
|
|
418
|
+
result.layout = preservation.layout;
|
|
419
|
+
else if (preservation.omitLayout)
|
|
420
|
+
delete result.layout;
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
function applyPreservedIdentities(result, preservation) {
|
|
424
|
+
const identities = preservation.identities;
|
|
425
|
+
if (!identities)
|
|
426
|
+
return;
|
|
427
|
+
const replacements = new Map();
|
|
428
|
+
const take = (current, wanted) => {
|
|
429
|
+
if (!current || !wanted || typeof current.id !== 'string')
|
|
430
|
+
return;
|
|
431
|
+
replacements.set(current.id, wanted);
|
|
432
|
+
current.id = wanted;
|
|
433
|
+
};
|
|
434
|
+
const takeConditions = (current, wanted) => {
|
|
435
|
+
(wanted ?? []).forEach((condition, index) => {
|
|
436
|
+
const target = current?.[index];
|
|
437
|
+
take(target, condition.id);
|
|
438
|
+
(condition.rules ?? []).forEach((group, groupIndex) => group.forEach((rule, ruleIndex) => take(target?.rules?.[groupIndex]?.[ruleIndex], rule.id)));
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
const takeSlas = (current, wanted) => {
|
|
442
|
+
(wanted ?? []).forEach((sla, index) => {
|
|
443
|
+
const target = current?.[index];
|
|
444
|
+
take(target, sla.id);
|
|
445
|
+
(sla.escalations ?? []).forEach((entry, escalationIndex) => take(target?.escalationRule?.[escalationIndex], entry.id));
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
take(result, identities.id);
|
|
449
|
+
const nodes = result.nodes;
|
|
450
|
+
const triggerNodes = nodes.filter((node) => node.type === 'case-management:Trigger' || node.type === 'uipath.case.trigger');
|
|
451
|
+
(identities.triggers ?? []).forEach((trigger, index) => take(triggerNodes[index], trigger.id));
|
|
452
|
+
const stages = nodes.filter((node) => String(node.type).endsWith('Stage'));
|
|
453
|
+
for (const stageIdentity of identities.stages ?? []) {
|
|
454
|
+
const stage = stages.find((candidate) => candidate.data?.label === stageIdentity.label);
|
|
455
|
+
take(stage, stageIdentity.id);
|
|
456
|
+
takeConditions(stage?.data?.entryConditions, stageIdentity.entryConditions);
|
|
457
|
+
takeConditions(stage?.data?.exitConditions, stageIdentity.exitConditions);
|
|
458
|
+
takeSlas(stage?.data?.slaRules, stageIdentity.slaRules);
|
|
459
|
+
(stageIdentity.tasks ?? []).forEach((lane, laneIndex) => lane.forEach((taskIdentity) => {
|
|
460
|
+
const task = stage?.data?.tasks?.[laneIndex]?.find((candidate) => candidate.displayName === taskIdentity.name);
|
|
461
|
+
take(task, taskIdentity.id);
|
|
462
|
+
takeConditions(task?.entryConditions, taskIdentity.entryConditions);
|
|
463
|
+
(taskIdentity.inputs ?? []).forEach((row, index) => take(task?.data?.inputs?.[index], row.id));
|
|
464
|
+
(taskIdentity.outputs ?? []).forEach((row, index) => {
|
|
465
|
+
const target = task?.data?.outputs?.[index];
|
|
466
|
+
const currentId = target?.id;
|
|
467
|
+
take(target, row.id);
|
|
468
|
+
if (typeof currentId === 'string' && typeof row.id === 'string' && target?.value === currentId) {
|
|
469
|
+
target.value = row.id;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
const metadata = result.metadata;
|
|
475
|
+
takeConditions(metadata.caseExitRules, identities.caseExitRules);
|
|
476
|
+
takeSlas(metadata.slaRules, identities.slaRules);
|
|
477
|
+
const variables = result.variables;
|
|
478
|
+
for (const [lane, rows] of Object.entries(identities.variables ?? {}))
|
|
479
|
+
(rows ?? []).forEach((row) => take(variables[lane]?.find((candidate) => candidate.name === row.name), row.id));
|
|
480
|
+
const replace = (value) => {
|
|
481
|
+
if (replacements.has(value))
|
|
482
|
+
return replacements.get(value);
|
|
483
|
+
let replaced = value;
|
|
484
|
+
for (const [current, wanted] of [...replacements].sort((a, b) => b[0].length - a[0].length))
|
|
485
|
+
replaced = replaced.replaceAll(current, wanted);
|
|
486
|
+
return replaced;
|
|
487
|
+
};
|
|
488
|
+
const referenceFields = new Set([
|
|
489
|
+
'elementId', 'selectedStageId', 'selectedStageIds', 'selectedTasksIds', 'slaId', 'escalationId',
|
|
490
|
+
'originalVar', 'var', 'source', 'target',
|
|
491
|
+
]);
|
|
492
|
+
const preservedOutputSources = [];
|
|
493
|
+
for (const node of nodes) {
|
|
494
|
+
if (String(node.type ?? '').endsWith('Stage')) {
|
|
495
|
+
for (const lane of node.data?.tasks ?? []) {
|
|
496
|
+
for (const task of lane) {
|
|
497
|
+
for (const row of task.data?.outputs ?? []) {
|
|
498
|
+
if (Object.prototype.hasOwnProperty.call(row, 'source')) {
|
|
499
|
+
preservedOutputSources.push({ row, source: row.source });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (node.type === 'case-management:Trigger' || node.type === 'uipath.case.trigger') {
|
|
507
|
+
const bag = node.type === 'uipath.case.trigger' ? node.data?.inputs : node.data?.uipath;
|
|
508
|
+
for (const row of bag?.outputs ?? []) {
|
|
509
|
+
if (typeof row.source === 'string' && !row.source.startsWith('=vars.')) {
|
|
510
|
+
preservedOutputSources.push({ row, source: row.source });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const rewrite = (value, field) => {
|
|
516
|
+
if (Array.isArray(value)) {
|
|
517
|
+
value.forEach((entry, index) => {
|
|
518
|
+
if (typeof entry === 'string' && field && referenceFields.has(field))
|
|
519
|
+
value[index] = replace(entry);
|
|
520
|
+
else
|
|
521
|
+
rewrite(entry, field);
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (!value || typeof value !== 'object')
|
|
526
|
+
return;
|
|
527
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
528
|
+
if (typeof entry === 'string' && referenceFields.has(name))
|
|
529
|
+
value[name] = replace(entry);
|
|
530
|
+
else
|
|
531
|
+
rewrite(entry, name);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
rewrite(result);
|
|
535
|
+
for (const { row, source } of preservedOutputSources)
|
|
536
|
+
row.source = source;
|
|
537
|
+
}
|
|
538
|
+
function emitVariables(vars, ids, triggerNodeByRef) {
|
|
539
|
+
const inputs = [];
|
|
540
|
+
const outputs = [];
|
|
541
|
+
const inputOutputs = [];
|
|
542
|
+
/**
|
|
543
|
+
* The root `inputOutputs` COMPANION — `id` = the variable's NAME (not its slot
|
|
544
|
+
* id) — is what an expression's `=js:vars.<name>` resolves against: the platform
|
|
545
|
+
* reads a variable by name from `nodes[root].inputOutputs`, and validates a
|
|
546
|
+
* reference to nothing (`vars.<name> does not exist`). Emitting it for every
|
|
547
|
+
* declared variable is the fix for the row-83 SDK-DEFECT — before this, a
|
|
548
|
+
* `.var()` / `.output()` companion carried `id: v<hash>`, so no gate could read
|
|
549
|
+
* it and mid-flight state had to be smuggled in as a trigger input to be
|
|
550
|
+
* readable at all. `elementId: 'root'` is the case root, which the trigger-bound
|
|
551
|
+
* In-arg path (below) mirrors with the trigger node's own id.
|
|
552
|
+
*/
|
|
553
|
+
// A structured (object/array) var persists as `type: 'jsonSchema'` with the shape
|
|
554
|
+
// in a JSON-schema `body` (mirrored into `_jsonSchema`, per the platform recipe).
|
|
555
|
+
const jsonSchemaFields = (vd) => vd.type === 'jsonSchema' && vd.body !== undefined ? { body: vd.body, _jsonSchema: vd.body } : {};
|
|
556
|
+
const encodedDefault = (vd) => {
|
|
557
|
+
if (vd.default === undefined)
|
|
558
|
+
return {};
|
|
559
|
+
if (typeof vd.default === 'string')
|
|
560
|
+
return { default: vd.default };
|
|
561
|
+
const value = JSON.stringify(vd.default);
|
|
562
|
+
if (value === undefined)
|
|
563
|
+
throw new TypeError(`Case variable "${vd.name}" has a default that cannot be JSON encoded.`);
|
|
564
|
+
return { default: value };
|
|
565
|
+
};
|
|
566
|
+
const readableCompanion = (vd) => ({
|
|
567
|
+
id: vd.name,
|
|
568
|
+
name: vd.name,
|
|
569
|
+
type: vd.type,
|
|
570
|
+
...encodedDefault(vd),
|
|
571
|
+
...jsonSchemaFields(vd),
|
|
572
|
+
elementId: 'root',
|
|
573
|
+
});
|
|
574
|
+
for (const vd of vars) {
|
|
575
|
+
const slotId = ids.make('v', 8, `var:${vd.name}`);
|
|
576
|
+
const entry = {
|
|
577
|
+
id: slotId,
|
|
578
|
+
name: vd.name,
|
|
579
|
+
type: vd.type,
|
|
580
|
+
...encodedDefault(vd),
|
|
581
|
+
};
|
|
582
|
+
Object.assign(entry, jsonSchemaFields(vd));
|
|
583
|
+
if (vd.direction === 'in' && vd.sourceTrigger) {
|
|
584
|
+
// Trigger-bound In-arg: formal slot + companion + a bridge on the trigger
|
|
585
|
+
// node that copies the slot into the companion at fire (see the skill's
|
|
586
|
+
// variables/io-binding + global-vars § In argument). Its companion is keyed
|
|
587
|
+
// to the trigger node, so it does not also take a root companion.
|
|
588
|
+
const node = triggerNodeByRef.get(vd.sourceTrigger);
|
|
589
|
+
if (!node)
|
|
590
|
+
throw new Error(`In-arg "${vd.name}" is bound via { from } to a trigger that was not added with .trigger(...).`);
|
|
591
|
+
entry.default = vd.default === undefined ? '' : encodedDefault(vd).default;
|
|
592
|
+
entry.elementId = node.id;
|
|
593
|
+
inputs.push(entry);
|
|
594
|
+
inputOutputs.push({ id: vd.name, name: vd.name, type: vd.type, elementId: node.id, ...jsonSchemaFields(vd) });
|
|
595
|
+
appendTriggerBridge(node, { name: vd.name, type: vd.type, source: `=vars.${slotId}`, var: vd.name });
|
|
596
|
+
}
|
|
597
|
+
else if (vd.direction === 'in') {
|
|
598
|
+
inputs.push(entry); // bare declaration (unbound In-arg; readable only via a trigger)
|
|
599
|
+
}
|
|
600
|
+
else if (vd.direction === 'out') {
|
|
601
|
+
outputs.push(entry); // formal declaration
|
|
602
|
+
inputOutputs.push(readableCompanion(vd)); // …made readable as `vars.<name>`
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
// inout: the root companion IS its declaration (nothing references the slot).
|
|
606
|
+
inputOutputs.push(readableCompanion(vd));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const out = { inputOutputs };
|
|
610
|
+
if (inputs.length)
|
|
611
|
+
out.inputs = inputs;
|
|
612
|
+
if (outputs.length)
|
|
613
|
+
out.outputs = outputs;
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Append an In-arg bridge to a trigger node's profile-owned payload. A manual
|
|
618
|
+
* trigger has no payload (its signature) — create `{ outputs: [] }` (with
|
|
619
|
+
* NO `serviceType`, so it stays manual) before appending.
|
|
620
|
+
*/
|
|
621
|
+
function appendTriggerBridge(node, bridge) {
|
|
622
|
+
const data = (node.data ?? (node.data = {}));
|
|
623
|
+
const bagField = CASE_FORMAT_PROFILE.triggerBagField;
|
|
624
|
+
const bag = (data[bagField] ?? (data[bagField] = { outputs: [] }));
|
|
625
|
+
if (!Array.isArray(bag.outputs))
|
|
626
|
+
bag.outputs = [];
|
|
627
|
+
bag.outputs.push(bridge);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Emit the case's trigger nodes. Empty → the default single manual `trigger_1`
|
|
631
|
+
* (back-compat). Otherwise one profile-owned trigger node per declared trigger:
|
|
632
|
+
* the first is `trigger_1` with no `parentElement` (the platform hydrates layout);
|
|
633
|
+
* the rest get a stable id + `parentElement`. A timer carries the profile-owned
|
|
634
|
+
* trigger bag; a manual omits the bag entirely (its signature).
|
|
635
|
+
*/
|
|
636
|
+
function emitTriggers(triggers, ids, resolver) {
|
|
637
|
+
const list = triggers.length ? triggers : [{ kind: 'manual' }];
|
|
638
|
+
return list.map((t, i) => {
|
|
639
|
+
const triggerId = i === 0 ? 'trigger_1' : ids.make('trigger_', 6, `trigger:${i}:${t.name ?? t.kind}`);
|
|
640
|
+
const data = {};
|
|
641
|
+
if (i > 0)
|
|
642
|
+
data.parentElement = { id: 'root', type: 'case-management:root' };
|
|
643
|
+
if (CASE_FORMAT_PROFILE.triggerTypeVersion)
|
|
644
|
+
data.typeVersion = CASE_FORMAT_PROFILE.triggerTypeVersion;
|
|
645
|
+
const label = t.name ?? `Trigger ${i + 1}`;
|
|
646
|
+
if (CASE_FORMAT_PROFILE.triggerLabelField === 'display')
|
|
647
|
+
data.display = { label };
|
|
648
|
+
else
|
|
649
|
+
data.label = label;
|
|
650
|
+
if (t.description)
|
|
651
|
+
data.description = t.description;
|
|
652
|
+
if (t.kind === 'timer') {
|
|
653
|
+
data[CASE_FORMAT_PROFILE.triggerBagField] = {
|
|
654
|
+
timerType: 'timeCycle',
|
|
655
|
+
timeCycle: t.timeCycle,
|
|
656
|
+
serviceType: CASE_FORMAT_PROFILE.timerServiceType,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
else if (t.kind === 'event') {
|
|
660
|
+
// With no subscription this remains the byte-identical, serviceType-only
|
|
661
|
+
// placeholder. A subscription resolves through the same library/binding
|
|
662
|
+
// machinery as connector tasks and wait-for-connector bags.
|
|
663
|
+
const bag = t.eventSubscription
|
|
664
|
+
? resolvedSubscriptionBag('trigger', t.eventSubscription, resolver, triggerId)
|
|
665
|
+
: { serviceType: CASE_FORMAT_PROFILE.eventServiceType };
|
|
666
|
+
if (t.eventOutputs?.length) {
|
|
667
|
+
const outputs = t.eventOutputs.map((o) => {
|
|
668
|
+
const field = o.source.replace(/^=/, '').split('.').pop() || o.source;
|
|
669
|
+
if (!resolver.varCompanions.has(o.var))
|
|
670
|
+
resolver.varCompanions.set(o.var, { id: o.var, name: o.var, type: o.type, elementId: 'root', custom: true });
|
|
671
|
+
return { name: field, var: o.var, source: o.source, type: o.type, value: o.var };
|
|
672
|
+
});
|
|
673
|
+
bag.outputs = [...(bag.outputs ?? []), ...outputs];
|
|
674
|
+
}
|
|
675
|
+
data[CASE_FORMAT_PROFILE.triggerBagField] = bag;
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
id: triggerId,
|
|
679
|
+
[CASE_FORMAT_PROFILE.nodeTypeField]: CASE_FORMAT_PROFILE.triggerNodeType,
|
|
680
|
+
data,
|
|
681
|
+
};
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function emitStage(stage, si, stageId, laneTaskIds, ids, resolver) {
|
|
685
|
+
const data = {
|
|
686
|
+
[CASE_FORMAT_PROFILE.stageLabelField]: stage.label,
|
|
687
|
+
parentElement: { id: 'root', type: 'case-management:root' },
|
|
688
|
+
isInvalidDropTarget: false,
|
|
689
|
+
isPendingParent: false,
|
|
690
|
+
tasks: stage.lanes.map((lane, li) => lane.map((task, ti) => emitTask(task, stage.label, laneTaskIds[li][ti], ids, resolver))),
|
|
691
|
+
};
|
|
692
|
+
if (stage.description)
|
|
693
|
+
data.description = stage.description;
|
|
694
|
+
if (stage.required !== undefined)
|
|
695
|
+
data.isRequired = stage.required;
|
|
696
|
+
if (stage.entry.length || stage.exception) {
|
|
697
|
+
data.entryConditions = stage.entry.map((c, i) => emitEntry(c, `${stage.label}:entry:${i}`, stageId, stage.label, ids, resolver));
|
|
698
|
+
}
|
|
699
|
+
if (stage.exit.length || stage.exception) {
|
|
700
|
+
data.exitConditions = stage.exit.map((c, i) => emitExit(c, `${stage.label}:exit:${i}`, stageId, stage.label, ids, resolver));
|
|
701
|
+
}
|
|
702
|
+
if (stage.slaRules.length) {
|
|
703
|
+
data.slaRules = stage.slaRules.map((s, i, all) => emitSla(s, i, all.length, ids, resolver));
|
|
704
|
+
}
|
|
705
|
+
const nodeType = stage.exception && CASE_FORMAT_PROFILE.exceptionStageField === CASE_FORMAT_PROFILE.nodeTypeField
|
|
706
|
+
? CASE_FORMAT_PROFILE.exceptionStageValue
|
|
707
|
+
: CASE_FORMAT_PROFILE.stageNodeType;
|
|
708
|
+
if (stage.exception && CASE_FORMAT_PROFILE.exceptionStageField === 'data.stageType')
|
|
709
|
+
data.stageType = CASE_FORMAT_PROFILE.exceptionStageValue;
|
|
710
|
+
return {
|
|
711
|
+
id: stageId,
|
|
712
|
+
[CASE_FORMAT_PROFILE.nodeTypeField]: nodeType,
|
|
713
|
+
...(CASE_FORMAT_PROFILE.layoutPolicy === 'inline' ? { position: { x: 100 + si * 500, y: 200 } } : {}),
|
|
714
|
+
data,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function emitTask(task, stageLabel, taskId, ids, resolver) {
|
|
718
|
+
const type = task.kind === 'connector' ? CASE_FORMAT_PROFILE.connectorActivityNodeType : task.kind;
|
|
719
|
+
const node = { id: taskId, displayName: task.name, [CASE_FORMAT_PROFILE.nodeTypeField]: type };
|
|
720
|
+
const stageId = resolver.stageIdByLabel.get(stageLabel);
|
|
721
|
+
node.data = taskData(task, resolver, taskId, stageId, ids);
|
|
722
|
+
// Every task carries `elementId: "<stageId>-<taskId>"`, the identity the
|
|
723
|
+
// converter matches a task's io rows against:
|
|
724
|
+
//
|
|
725
|
+
// const outputs = taskOutputs.filter(
|
|
726
|
+
// (output) => task.elementId === output.elementId && !!output.id?.length);
|
|
727
|
+
//
|
|
728
|
+
// …in `collectCaseTaskOutputGroups`, which feeds `getAllTasksOutputVariables`
|
|
729
|
+
// and `collectOverlayOutputs`. The io ROWS have always carried the value (see
|
|
730
|
+
// `taskData` below), so leaving it off the task made that filter compare
|
|
731
|
+
// `undefined` against the row's id, drop every match, and emit no
|
|
732
|
+
// `uipath:output` bridge — a declared `.outputs()` became inert at runtime
|
|
733
|
+
// while `validate` reported only a warning (TASK_MISSING_ELEMENT_ID).
|
|
734
|
+
//
|
|
735
|
+
// Emitted for every kind, not just those carrying io: that is what the modern
|
|
736
|
+
// designer writes (every task in the V27 corpus has one, always this
|
|
737
|
+
// spelling), and it is provably inert where nothing reads it — stamping it on
|
|
738
|
+
// a bare wait-for-timer leaves the converter's BPMN and rules byte-identical.
|
|
739
|
+
node.elementId = `${stageId}-${taskId}`;
|
|
740
|
+
if (task.runOnce !== undefined)
|
|
741
|
+
node.shouldRunOnlyOnce = task.runOnce;
|
|
742
|
+
if (task.required !== undefined)
|
|
743
|
+
node.isRequired = task.required;
|
|
744
|
+
if (task.skip)
|
|
745
|
+
node.skipCondition = task.skip;
|
|
746
|
+
if (task.description)
|
|
747
|
+
node.description = task.description;
|
|
748
|
+
if (task.entry.length) {
|
|
749
|
+
node.entryConditions = task.entry.map((c, i) => emitTaskEntry(c, `${stageLabel}/${task.name}:entry:${i}`, stageId, task.name, ids, resolver));
|
|
750
|
+
}
|
|
751
|
+
return node;
|
|
752
|
+
}
|
|
753
|
+
function taskData(task, resolver, taskId, stageId, ids) {
|
|
754
|
+
switch (task.kind) {
|
|
755
|
+
case 'process':
|
|
756
|
+
case 'agent':
|
|
757
|
+
case 'rpa':
|
|
758
|
+
case 'api-workflow':
|
|
759
|
+
// A `case-management` (sub-case) task invokes another published case, and its
|
|
760
|
+
// data shape is the same reference-mode `RunProcessTaskData` — so passing data
|
|
761
|
+
// into the child (`data.inputs[]`) and reading results back (`data.outputs[]`)
|
|
762
|
+
// uses the identical io-binding wire as the other reference tasks (Gap F).
|
|
763
|
+
case 'case-management':
|
|
764
|
+
case 'flow-process': {
|
|
765
|
+
// Reference-mode task, plus optional io-binding: input params and output
|
|
766
|
+
// extractions (`data.inputs[]` / `data.outputs[]`). Each row's `elementId`
|
|
767
|
+
// is "<stageId>-<taskId>"; an output also registers a root `inputOutputs`
|
|
768
|
+
// companion so `=vars.<var>` resolves downstream.
|
|
769
|
+
if (!task.ref)
|
|
770
|
+
return {};
|
|
771
|
+
const data = { name: task.ref.name, folderPath: task.ref.folderPath };
|
|
772
|
+
const elementId = `${stageId}-${taskId}`;
|
|
773
|
+
if (task.inputs?.length)
|
|
774
|
+
data.inputs = task.inputs.map((inp, i) => emitTaskInput(inp, elementId, ids, `${elementId}:in:${i}:${inp.name}`));
|
|
775
|
+
if (task.outputs?.length)
|
|
776
|
+
data.outputs = task.outputs.map((o, i) => emitTaskOutput(o, elementId, ids, `${elementId}:out:${i}:${o.var}`, resolver));
|
|
777
|
+
return data;
|
|
778
|
+
}
|
|
779
|
+
case 'action': {
|
|
780
|
+
const a = task.action ?? {};
|
|
781
|
+
const data = {};
|
|
782
|
+
if (a.title)
|
|
783
|
+
data.taskTitle = a.title;
|
|
784
|
+
data.priority = a.priority ?? 'Medium';
|
|
785
|
+
if (a.recipient) {
|
|
786
|
+
data.assignmentCriteria = 'user';
|
|
787
|
+
data.recipient = { Type: a.recipient.type, Value: a.recipient.value };
|
|
788
|
+
}
|
|
789
|
+
if (a.labels)
|
|
790
|
+
data.labels = a.labels;
|
|
791
|
+
if (a.actionCatalogName)
|
|
792
|
+
data.actionCatalogName = a.actionCatalogName;
|
|
793
|
+
// HITL form fields: inputs are read-only context the assignee sees, outputs
|
|
794
|
+
// are what they fill in. Each is a schema `InputOutput` row.
|
|
795
|
+
if (a.inputs?.length)
|
|
796
|
+
data.inputs = a.inputs.map((field) => emitActionField(field));
|
|
797
|
+
if (a.outputs?.length) {
|
|
798
|
+
data.outputs = a.outputs.map((field) => emitActionField(field, `${stageId}-${taskId}`, resolver));
|
|
799
|
+
}
|
|
800
|
+
return data;
|
|
801
|
+
}
|
|
802
|
+
case 'connector':
|
|
803
|
+
return connectorTaskData(task.connector, resolver, taskId);
|
|
804
|
+
case 'external-agent':
|
|
805
|
+
case 'external-workflow':
|
|
806
|
+
return externalTaskData(task.kind, task.external, resolver);
|
|
807
|
+
case 'wait-for-timer': {
|
|
808
|
+
if (task.timer?.duration)
|
|
809
|
+
return { timerType: 'timeDuration', timeDuration: task.timer.duration };
|
|
810
|
+
if (task.timer?.date)
|
|
811
|
+
return { timerType: 'timeDate', timeDate: task.timer.date };
|
|
812
|
+
if (task.timer?.cycle)
|
|
813
|
+
return { timerType: 'timeCycle', timeCycle: task.timer.cycle };
|
|
814
|
+
return {};
|
|
815
|
+
}
|
|
816
|
+
case 'wait-for-connector':
|
|
817
|
+
// Floor placeholder (no connector named) = `serviceType` only; naming the
|
|
818
|
+
// connector/operation emits the `context` subscription.
|
|
819
|
+
return waitConnectorBag(task.waitConnector, { alwaysContext: false }, resolver, `${stageId}-${taskId}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* The `wait-for-connector` subscription bag (`data` on the task, `uipath` on the
|
|
824
|
+
* rule). Verified against a real `uip maestro case` caseplan: a placeholder
|
|
825
|
+
* carries `serviceType: "Intsvc.WaitForEvent"`; naming the connector adds a
|
|
826
|
+
* two-entry `context` (connectorKey + operation, defaulting to `"placeholder"`)
|
|
827
|
+
* and empty `inputs`/`outputs`/`bindings`. A *rule* always carries the context —
|
|
828
|
+
* `validate` rejects a bare `uipath` ("connector activity missing").
|
|
829
|
+
*/
|
|
830
|
+
function waitConnectorBag(spec, opts, resolver, elementId) {
|
|
831
|
+
if (isResolvedSubscription(spec))
|
|
832
|
+
return resolvedSubscriptionBag('event', spec, resolver, elementId);
|
|
833
|
+
const bag = { serviceType: CASE_FORMAT_PROFILE.waitConnectorServiceType };
|
|
834
|
+
const named = spec?.connectorKey !== undefined || spec?.operation !== undefined;
|
|
835
|
+
if (opts.alwaysContext || named) {
|
|
836
|
+
bag.context = [
|
|
837
|
+
{ name: 'connectorKey', type: 'string', value: spec?.connectorKey ?? 'placeholder' },
|
|
838
|
+
{ name: 'operation', type: 'string', value: spec?.operation ?? 'placeholder' },
|
|
839
|
+
];
|
|
840
|
+
bag.inputs = [];
|
|
841
|
+
bag.outputs = [];
|
|
842
|
+
bag.bindings = [];
|
|
843
|
+
}
|
|
844
|
+
return bag;
|
|
845
|
+
}
|
|
846
|
+
function isResolvedSubscription(spec) {
|
|
847
|
+
return !!spec && 'connector' in spec && 'event' in spec;
|
|
848
|
+
}
|
|
849
|
+
/** Resolve a Case event/wait subscription into the designer/runtime bag pinned
|
|
850
|
+
* by the archived v1 artifacts. Placeholder bags never enter this path. */
|
|
851
|
+
function resolvedSubscriptionBag(kind, spec, resolver, elementId) {
|
|
852
|
+
if (!resolver.library)
|
|
853
|
+
throw new Error(`${kind === 'trigger' ? 'event trigger' : 'wait-for-connector'} needs the connector library — `
|
|
854
|
+
+ `pass { library } to serialize or use uip maestro case compile --library.`);
|
|
855
|
+
const rc = resolver.library.resolveEvent(kind, spec.connector, spec.event, spec.version);
|
|
856
|
+
const op = rc.entry.operation ?? {};
|
|
857
|
+
const context = [io('connectorKey', 'string', { value: rc.connectorKey })];
|
|
858
|
+
const connection = bindingRef(resolver, spec.connection, 'Connection', 'ConnectionId', `${rc.connectorKey} connection`);
|
|
859
|
+
if (connection) {
|
|
860
|
+
context.push(io('connection', 'string', { value: `=bindings.${connection.id}` }));
|
|
861
|
+
context.push(io('resourceKey', 'string', { value: connection.resourceKey }));
|
|
862
|
+
}
|
|
863
|
+
const folder = bindingRef(resolver, spec.folder, 'Connection', 'folderKey', 'FolderKey');
|
|
864
|
+
if (folder)
|
|
865
|
+
context.push(io('folderKey', 'string', { value: `=bindings.${folder.id}` }));
|
|
866
|
+
context.push(io('method', 'string', {}), io('path', 'string', {}), io('objectName', 'string', { value: op.objectName ?? spec.event }), io('operation', 'string', { value: op.eventOperation ?? spec.event }), io('metadata', 'json', { body: subscriptionMetadata(rc, op, spec.filters ?? [], elementId) }));
|
|
867
|
+
return {
|
|
868
|
+
serviceType: kind === 'trigger' ? CASE_FORMAT_PROFILE.eventServiceType : CASE_FORMAT_PROFILE.waitConnectorServiceType,
|
|
869
|
+
context,
|
|
870
|
+
inputs: [eventInput(spec.where ?? {}, elementId)],
|
|
871
|
+
outputs: connectorOutputs(rc, elementId, resolver),
|
|
872
|
+
bindings: [],
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function eventInput(where, elementId) {
|
|
876
|
+
// Case compile does not read resolutions.json (Flow does); a token here
|
|
877
|
+
// would be emitted raw where the connector expects an id.
|
|
878
|
+
for (const [field, value] of Object.entries(where))
|
|
879
|
+
refuseLookupToken(value, 'case', field);
|
|
880
|
+
return Object.keys(where).length
|
|
881
|
+
? { name: 'body', type: 'json', target: 'body', body: { queryParams: where }, var: 'vbody', id: 'vbody', elementId }
|
|
882
|
+
: { name: 'body', type: 'json', target: 'body', value: '', var: 'vbody', id: 'vbody', elementId };
|
|
883
|
+
}
|
|
884
|
+
function subscriptionMetadata(rc, op, filters, elementId) {
|
|
885
|
+
const instanceParameters = {
|
|
886
|
+
connectorKey: rc.connectorKey,
|
|
887
|
+
objectName: op.objectName ?? rc.nodeType.split('.').pop(),
|
|
888
|
+
activityType: op.activityType,
|
|
889
|
+
version: rc.version,
|
|
890
|
+
eventOperation: op.eventOperation,
|
|
891
|
+
eventMode: op.eventMode,
|
|
892
|
+
supportsStreaming: op.supportsStreaming ?? false,
|
|
893
|
+
};
|
|
894
|
+
const filter = filters.length ? {
|
|
895
|
+
groupOperator: 0,
|
|
896
|
+
index: 0,
|
|
897
|
+
filters: filters.map((entry, index) => {
|
|
898
|
+
// Same authored shape as a flow's event filter, same designer grammar (see
|
|
899
|
+
// `event-filters.ts`); a malformed leaf is refused rather than dropped.
|
|
900
|
+
const { read } = readEventFilter(entry);
|
|
901
|
+
if (!read)
|
|
902
|
+
throw new Error(`event filter ${index} must carry a field and exactly one operator — got ${JSON.stringify(entry)}.`);
|
|
903
|
+
const problem = eventFilterProblem(read);
|
|
904
|
+
if (problem)
|
|
905
|
+
throw new Error(`event filter ${index} on \`${read.field}\`: ${problem}`);
|
|
906
|
+
return {
|
|
907
|
+
...eventFilterTreeLeaf(read),
|
|
908
|
+
uiId: stableUuid(`${elementId}:${read.field}:${String(read.value)}:${index}`),
|
|
909
|
+
};
|
|
910
|
+
}).map((leaf) => ({
|
|
911
|
+
id: leaf.id,
|
|
912
|
+
operator: leaf.operator,
|
|
913
|
+
value: leaf.value,
|
|
914
|
+
uiId: leaf.uiId,
|
|
915
|
+
})),
|
|
916
|
+
groups: [],
|
|
917
|
+
uuId: stableUuid(`${elementId}:filters`),
|
|
918
|
+
} : null;
|
|
919
|
+
const configuration = {
|
|
920
|
+
essentialConfiguration: {
|
|
921
|
+
connectorVersion: null,
|
|
922
|
+
executionType: null,
|
|
923
|
+
customFieldsRequestDetails: null,
|
|
924
|
+
instanceParameters,
|
|
925
|
+
objectName: instanceParameters.objectName,
|
|
926
|
+
packageVersion: rc.version,
|
|
927
|
+
httpMethod: null,
|
|
928
|
+
path: null,
|
|
929
|
+
filter,
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
return {
|
|
933
|
+
activityPropertyConfiguration: {
|
|
934
|
+
objectName: instanceParameters.objectName,
|
|
935
|
+
eventType: op.eventOperation,
|
|
936
|
+
eventMode: op.eventMode,
|
|
937
|
+
configuration: `=jsonString:${JSON.stringify(configuration)}`,
|
|
938
|
+
uiPathActivityTypeId: rc.activityTypeId ?? '',
|
|
939
|
+
errorState: { issues: [] },
|
|
940
|
+
},
|
|
941
|
+
activityMetadata: {
|
|
942
|
+
activity: {
|
|
943
|
+
displayName: rc.entry.display?.label ?? instanceParameters.objectName,
|
|
944
|
+
description: rc.entry.display?.description ?? '',
|
|
945
|
+
category: rc.connectorName,
|
|
946
|
+
configuration: JSON.stringify(instanceParameters),
|
|
947
|
+
uiPathActivityTypeId: rc.activityTypeId ?? '',
|
|
948
|
+
isEnabled: true,
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
telemetryData: {
|
|
952
|
+
connectorKey: rc.connectorKey,
|
|
953
|
+
connectorName: rc.connectorName,
|
|
954
|
+
objectName: instanceParameters.objectName,
|
|
955
|
+
objectDisplayName: op.objectDisplayName ?? rc.entry.display?.operationLabel ?? '',
|
|
956
|
+
},
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function stableUuid(seed) {
|
|
960
|
+
let h1 = 0x811c9dc5;
|
|
961
|
+
let h2 = 0x01000193;
|
|
962
|
+
for (let index = 0; index < seed.length; index++) {
|
|
963
|
+
h1 = (h1 ^ seed.charCodeAt(index)) * 0x01000193 >>> 0;
|
|
964
|
+
h2 = (h2 + seed.charCodeAt(index) * (index + 7)) >>> 0;
|
|
965
|
+
}
|
|
966
|
+
const hex = (value, length) => value.toString(16).padStart(8, '0').slice(0, length);
|
|
967
|
+
return `${hex(h1, 8)}-${hex(h2, 4)}-4${hex(h1 >>> 8, 3)}-8${hex(h2 >>> 8, 3)}-${hex(h1 ^ h2, 8)}${hex(h2, 4)}`;
|
|
968
|
+
}
|
|
969
|
+
// ─── Reference-mode task io-binding (data.inputs[] / data.outputs[]) ───────────
|
|
970
|
+
//
|
|
971
|
+
// Wire shape verified against a real `uip maestro case` caseplan (the io-binding
|
|
972
|
+
// matrix). Each row is `Variable`-shaped; `elementId` is "<stageId>-<taskId>". An
|
|
973
|
+
// input binds a resource param to a literal/`=vars.X`/`=js:` value; an output
|
|
974
|
+
// extracts a result field (`source`) into a case variable (`var`), registering a
|
|
975
|
+
// root `inputOutputs` companion (`id === var`) so `=vars.<var>` resolves.
|
|
976
|
+
/**
|
|
977
|
+
* One action-task form field → a schema `InputOutput` row. Unlike reference-mode
|
|
978
|
+
* io-binding, most form fields carry only a name + type plus optional Action
|
|
979
|
+
* Center metadata. A designer output with `value` names its readable case
|
|
980
|
+
* variable and therefore also carries the standard output identity fields.
|
|
981
|
+
*/
|
|
982
|
+
function emitActionField(f, outputElementId, resolver) {
|
|
983
|
+
const row = { name: f.name, type: f.type ?? 'string' };
|
|
984
|
+
if (f.subType !== undefined)
|
|
985
|
+
row.subType = f.subType;
|
|
986
|
+
if (f.displayName !== undefined)
|
|
987
|
+
row.displayName = f.displayName;
|
|
988
|
+
if (f.value !== undefined)
|
|
989
|
+
row.value = f.value;
|
|
990
|
+
if (outputElementId !== undefined && f.value !== undefined) {
|
|
991
|
+
row.id = f.value;
|
|
992
|
+
row.var = f.value;
|
|
993
|
+
row.source = `=${f.name}`;
|
|
994
|
+
row.target = `=${f.value}`;
|
|
995
|
+
row.elementId = outputElementId;
|
|
996
|
+
if (resolver && !resolver.varCompanions.has(f.value)) {
|
|
997
|
+
resolver.varCompanions.set(f.value, {
|
|
998
|
+
id: f.value,
|
|
999
|
+
name: f.value,
|
|
1000
|
+
type: f.type ?? 'string',
|
|
1001
|
+
elementId: 'root',
|
|
1002
|
+
custom: true,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (f.required !== undefined)
|
|
1007
|
+
row.required = f.required;
|
|
1008
|
+
if (f.options !== undefined)
|
|
1009
|
+
row.options = f.options.map((option) => ({ value: option.value, label: option.label }));
|
|
1010
|
+
return row;
|
|
1011
|
+
}
|
|
1012
|
+
/** One `data.inputs[]` row: a resource input param set to `value`. */
|
|
1013
|
+
function emitTaskInput(inp, elementId, ids, seed) {
|
|
1014
|
+
const slot = ids.make('v', 8, seed);
|
|
1015
|
+
return { name: inp.name, type: inp.type, id: slot, var: slot, elementId, value: inp.value };
|
|
1016
|
+
}
|
|
1017
|
+
/** One `data.outputs[]` row (the extract shape): `source` field → the `var` case variable. */
|
|
1018
|
+
function emitTaskOutput(o, elementId, ids, seed, resolver) {
|
|
1019
|
+
const slot = ids.make('o', 8, seed);
|
|
1020
|
+
const field = o.source.replace(/^=/, '').split('.').pop() || o.source;
|
|
1021
|
+
// Register the readable companion once (declared vars take precedence in the merge).
|
|
1022
|
+
if (!resolver.varCompanions.has(o.var))
|
|
1023
|
+
resolver.varCompanions.set(o.var, { id: o.var, name: o.var, type: o.type, elementId: 'root', custom: true });
|
|
1024
|
+
return {
|
|
1025
|
+
name: field,
|
|
1026
|
+
type: o.type,
|
|
1027
|
+
id: slot,
|
|
1028
|
+
var: o.var,
|
|
1029
|
+
originalVar: slot,
|
|
1030
|
+
value: o.var,
|
|
1031
|
+
source: o.source,
|
|
1032
|
+
target: `=${slot}`,
|
|
1033
|
+
elementId,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
// ─── Connector task (execute-connector-activity) ──────────────────────────────
|
|
1037
|
+
/**
|
|
1038
|
+
* Build the `execute-connector-activity` `data` payload for an IS connector task,
|
|
1039
|
+
* from the same `ResolvedConnector` the Flow node uses (verified against a real
|
|
1040
|
+
* `uip maestro case` caseplan — see docs/CONNECTORS_DESIGN.md §C). The connector
|
|
1041
|
+
* wiring lives in `context[]` (connectorKey/objectName/method/path + the
|
|
1042
|
+
* essentialConfiguration blob); `inputs[]` splits into pathParameters/
|
|
1043
|
+
* queryParameters/body by the op's declared parameters; `outputs[]` comes from
|
|
1044
|
+
* the op's output schema. Connection/folder resolve through `bindings` and are
|
|
1045
|
+
* referenced as `=bindings.<id>`, with the binding entry collected at case root.
|
|
1046
|
+
*/
|
|
1047
|
+
function connectorTaskData(spec, resolver, elementId) {
|
|
1048
|
+
if (!resolver.library)
|
|
1049
|
+
throw new Error(`connector task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
|
|
1050
|
+
const rc = resolver.library.resolve(spec.key, spec.action, spec.version, spec.object);
|
|
1051
|
+
const op = rc.entry.operation ?? {};
|
|
1052
|
+
const params = Array.isArray(op.parameters) ? op.parameters : [];
|
|
1053
|
+
const context = [
|
|
1054
|
+
io('connectorKey', 'string', { value: rc.connectorKey }),
|
|
1055
|
+
];
|
|
1056
|
+
// connection + folder → root bindings, referenced by id.
|
|
1057
|
+
const connRef = bindingRef(resolver, spec.connection, 'Connection', 'ConnectionId', `${rc.connectorKey} connection`);
|
|
1058
|
+
if (connRef) {
|
|
1059
|
+
context.push(io('connection', 'string', { value: `=bindings.${connRef.id}` }));
|
|
1060
|
+
context.push(io('resourceKey', 'string', { value: connRef.resourceKey }));
|
|
1061
|
+
}
|
|
1062
|
+
const folderRef = bindingRef(resolver, spec.folder, 'Connection', 'folderKey', 'FolderKey');
|
|
1063
|
+
if (folderRef)
|
|
1064
|
+
context.push(io('folderKey', 'string', { value: `=bindings.${folderRef.id}` }));
|
|
1065
|
+
context.push(io('objectName', 'string', { value: op.objectName ?? spec.action }), io('method', 'string', { value: transportHttpMethod(rc.httpMethod) }), io('path', 'string', { value: rc.endpoint }), io('metadata', 'json', { body: connectorMetadata(rc, op) }));
|
|
1066
|
+
// inputs: split declared parameters into path/query slots; the rest is the body.
|
|
1067
|
+
//
|
|
1068
|
+
// Guard BEFORE the split. These values are emitted verbatim, so an unresolved
|
|
1069
|
+
// lookup ships as `{"__lookup": …}` where the connector wants an id — verified
|
|
1070
|
+
// by probe before this existed. Only Flow reads resolutions.json.
|
|
1071
|
+
for (const [name, value] of Object.entries(spec.inputs ?? {})) {
|
|
1072
|
+
refuseLookupToken(value, 'case', name);
|
|
1073
|
+
}
|
|
1074
|
+
const pathParameters = {};
|
|
1075
|
+
const queryParameters = {};
|
|
1076
|
+
const body = { ...spec.inputs };
|
|
1077
|
+
for (const p of params) {
|
|
1078
|
+
const name = String(p.name ?? '');
|
|
1079
|
+
if (!(name in body))
|
|
1080
|
+
continue;
|
|
1081
|
+
const camel = toCamel(name);
|
|
1082
|
+
if (p.type === 'path') {
|
|
1083
|
+
pathParameters[camel] = body[name];
|
|
1084
|
+
delete body[name];
|
|
1085
|
+
}
|
|
1086
|
+
else if (p.type === 'query') {
|
|
1087
|
+
queryParameters[camel] = body[name];
|
|
1088
|
+
delete body[name];
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const inputs = [
|
|
1092
|
+
ioSlot('pathParameters', pathParameters, `${elementId}`),
|
|
1093
|
+
ioSlot('queryParameters', queryParameters, `${elementId}`),
|
|
1094
|
+
ioSlot('body', body, `${elementId}`),
|
|
1095
|
+
];
|
|
1096
|
+
const outputs = connectorOutputs(rc, elementId, resolver);
|
|
1097
|
+
return { serviceType: CASE_FORMAT_PROFILE.connectorActivityServiceType, context, inputs, outputs, bindings: [] };
|
|
1098
|
+
}
|
|
1099
|
+
function externalTaskData(kind, spec, resolver) {
|
|
1100
|
+
if (!resolver.library)
|
|
1101
|
+
throw new Error(`${kind} task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
|
|
1102
|
+
const rc = resolver.library.resolve(spec.connector.key, spec.connector.action, spec.connector.version, spec.connector.object);
|
|
1103
|
+
const op = rc.entry.operation ?? {};
|
|
1104
|
+
const expectedSubType = kind === 'external-agent' ? 'AgentExecution' : 'ProcessExecution';
|
|
1105
|
+
if (op.subType !== expectedSubType)
|
|
1106
|
+
throw new Error(`${kind} task requires a ${expectedSubType} descriptor; resolved ${String(op.subType ?? 'no subtype')}.`);
|
|
1107
|
+
if (!rc.activityTypeId)
|
|
1108
|
+
throw new Error(`${kind} task descriptor ${rc.nodeType}@${rc.version} has no uiPathActivityTypeId.`);
|
|
1109
|
+
if (spec.mode !== 'sync' && spec.mode !== 'async')
|
|
1110
|
+
throw new Error(`${kind} task mode must be "sync" or "async".`);
|
|
1111
|
+
const connection = bindingRef(resolver, spec.connector.connection, 'Connection', 'ConnectionId', `${rc.connectorKey} connection`);
|
|
1112
|
+
const folder = bindingRef(resolver, spec.connector.folder, 'Connection', 'folderKey', 'FolderKey');
|
|
1113
|
+
if (!connection || !folder)
|
|
1114
|
+
throw new Error(`${kind} task requires non-empty connection and folder binding names.`);
|
|
1115
|
+
const serviceType = kind === 'external-agent'
|
|
1116
|
+
? (spec.mode === 'sync'
|
|
1117
|
+
? CASE_FORMAT_PROFILE.externalAgentSyncServiceType
|
|
1118
|
+
: CASE_FORMAT_PROFILE.externalAgentAsyncServiceType)
|
|
1119
|
+
: (spec.mode === 'sync'
|
|
1120
|
+
? CASE_FORMAT_PROFILE.externalWorkflowSyncServiceType
|
|
1121
|
+
: CASE_FORMAT_PROFILE.externalWorkflowAsyncServiceType);
|
|
1122
|
+
const fields = new Map(rc.fields.map((field) => [field.name, field]));
|
|
1123
|
+
const unknown = Object.keys(spec.connector.inputs).find((name) => !fields.has(name));
|
|
1124
|
+
if (unknown)
|
|
1125
|
+
throw new Error(`${kind} task has unknown input "${unknown}" for ${rc.nodeType}@${rc.version}.`);
|
|
1126
|
+
const missing = rc.fields.find((field) => field.required && !(field.name in spec.connector.inputs));
|
|
1127
|
+
if (missing)
|
|
1128
|
+
throw new Error(`${kind} task is missing required input "${missing.name}".`);
|
|
1129
|
+
const responseVar = connectorOutputVar(resolver, 'response');
|
|
1130
|
+
return {
|
|
1131
|
+
serviceType,
|
|
1132
|
+
context: [
|
|
1133
|
+
io('activity', 'string', { value: rc.activityTypeId }),
|
|
1134
|
+
io('connectorKey', 'string', { value: rc.connectorKey }),
|
|
1135
|
+
io('connection', 'string', { value: `=bindings.${connection.id}` }),
|
|
1136
|
+
io('folderKey', 'string', { value: `=bindings.${folder.id}` }),
|
|
1137
|
+
io('method', 'string', { value: transportHttpMethod(rc.httpMethod) }),
|
|
1138
|
+
io('path', 'string', { value: rc.endpoint }),
|
|
1139
|
+
],
|
|
1140
|
+
inputs: Object.entries(spec.connector.inputs).map(([name, value]) => ({
|
|
1141
|
+
id: name,
|
|
1142
|
+
name,
|
|
1143
|
+
type: jsonType(fields.get(name)?.type),
|
|
1144
|
+
value,
|
|
1145
|
+
target: name,
|
|
1146
|
+
})),
|
|
1147
|
+
outputs: [{
|
|
1148
|
+
id: responseVar,
|
|
1149
|
+
name: 'response',
|
|
1150
|
+
type: 'json',
|
|
1151
|
+
source: '.',
|
|
1152
|
+
var: responseVar,
|
|
1153
|
+
}],
|
|
1154
|
+
bindings: [],
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/** A context/output name/value entry (the caseplan `InputOutput` shape). */
|
|
1158
|
+
function io(name, type, extra) {
|
|
1159
|
+
return { name, type, ...extra };
|
|
1160
|
+
}
|
|
1161
|
+
/** An `inputs[]` json slot (pathParameters/queryParameters/body). */
|
|
1162
|
+
function ioSlot(name, bodyObj, elementId) {
|
|
1163
|
+
const v = `v${name}`;
|
|
1164
|
+
return { name, type: 'json', target: name, body: bodyObj, var: v, id: v, elementId };
|
|
1165
|
+
}
|
|
1166
|
+
/** Resolve a symbolic connection/folder to a root binding entry (collected once),
|
|
1167
|
+
* returning its id + resourceKey for the `=bindings.<id>` reference. */
|
|
1168
|
+
function bindingRef(resolver, name, resource, propertyAttribute, displayName) {
|
|
1169
|
+
if (!name)
|
|
1170
|
+
return undefined;
|
|
1171
|
+
const entry = resolver.bindings?.entry(name);
|
|
1172
|
+
const id = entry?.id ?? name;
|
|
1173
|
+
const resourceKey = entry?.resourceKey ?? entry?.default ?? name;
|
|
1174
|
+
if (!resolver.bindingSink.has(id)) {
|
|
1175
|
+
resolver.bindingSink.set(id, {
|
|
1176
|
+
id,
|
|
1177
|
+
name: entry?.name ?? displayName,
|
|
1178
|
+
type: 'string',
|
|
1179
|
+
resource,
|
|
1180
|
+
resourceKey,
|
|
1181
|
+
default: entry?.default ?? resourceKey,
|
|
1182
|
+
propertyAttribute,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
return { id, resourceKey };
|
|
1186
|
+
}
|
|
1187
|
+
/** The connector `metadata` json blob: the FUNCTIONAL essentialConfiguration +
|
|
1188
|
+
* activityTypeId. Designer cosmetics (packageId/activityColor/…) are omitted —
|
|
1189
|
+
* the caseplan schema-validates without them (see design §C). */
|
|
1190
|
+
function connectorMetadata(rc, op) {
|
|
1191
|
+
const instanceParameters = {
|
|
1192
|
+
connectorKey: rc.connectorKey,
|
|
1193
|
+
objectName: op.objectName ?? rc.nodeType.split('.').pop(),
|
|
1194
|
+
httpMethod: rc.httpMethod,
|
|
1195
|
+
activityType: op.activityType ?? 'Curated',
|
|
1196
|
+
version: rc.version,
|
|
1197
|
+
supportsStreaming: op.supportsStreaming ?? false,
|
|
1198
|
+
subType: op.subType ?? 'standard',
|
|
1199
|
+
};
|
|
1200
|
+
const configuration = {
|
|
1201
|
+
essentialConfiguration: {
|
|
1202
|
+
instanceParameters,
|
|
1203
|
+
objectName: instanceParameters.objectName,
|
|
1204
|
+
httpMethod: transportHttpMethod(rc.httpMethod),
|
|
1205
|
+
path: rc.endpoint,
|
|
1206
|
+
packageVersion: rc.version,
|
|
1207
|
+
},
|
|
1208
|
+
};
|
|
1209
|
+
return {
|
|
1210
|
+
activityPropertyConfiguration: {
|
|
1211
|
+
configuration: `=jsonString:${JSON.stringify(configuration)}`,
|
|
1212
|
+
uiPathActivityTypeId: rc.activityTypeId ?? '',
|
|
1213
|
+
errorState: { issues: [] },
|
|
1214
|
+
},
|
|
1215
|
+
activityMetadata: {
|
|
1216
|
+
activity: {
|
|
1217
|
+
displayName: rc.entry.display?.label ?? instanceParameters.objectName,
|
|
1218
|
+
description: rc.entry.display?.description ?? '',
|
|
1219
|
+
category: rc.connectorName,
|
|
1220
|
+
configuration: JSON.stringify(instanceParameters),
|
|
1221
|
+
uiPathActivityTypeId: rc.activityTypeId ?? '',
|
|
1222
|
+
isEnabled: true,
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
telemetryData: {
|
|
1226
|
+
connectorKey: rc.connectorKey,
|
|
1227
|
+
connectorName: rc.connectorName,
|
|
1228
|
+
objectName: instanceParameters.objectName,
|
|
1229
|
+
objectDisplayName: op.objectDisplayName ?? rc.entry.display?.operationLabel ?? '',
|
|
1230
|
+
},
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
/** The `outputs[]`: a `response` json-schema from the op's output schema, plus a
|
|
1234
|
+
* standard `Error` schema. */
|
|
1235
|
+
function connectorOutputVar(resolver, base) {
|
|
1236
|
+
const occurrence = (resolver.connectorOutputCounts.get(base) ?? 0) + 1;
|
|
1237
|
+
resolver.connectorOutputCounts.set(base, occurrence);
|
|
1238
|
+
return occurrence === 1 ? base : `${base}${occurrence}`;
|
|
1239
|
+
}
|
|
1240
|
+
function connectorOutputs(rc, elementId, resolver) {
|
|
1241
|
+
const responseSchema = rc.entry.outputSchema
|
|
1242
|
+
? schemaFromFields(rc.entry.outputSchema.fields ?? [])
|
|
1243
|
+
: { type: 'object', properties: {} };
|
|
1244
|
+
const responseVar = connectorOutputVar(resolver, 'response');
|
|
1245
|
+
const errorVar = connectorOutputVar(resolver, 'Error');
|
|
1246
|
+
return [
|
|
1247
|
+
{
|
|
1248
|
+
name: 'response', displayName: 'Response', type: 'jsonSchema', source: '=response',
|
|
1249
|
+
body: { $schema: 'http://json-schema.org/draft-07/schema#', ...responseSchema },
|
|
1250
|
+
var: responseVar, id: responseVar, value: responseVar, target: `=${responseVar}`, elementId,
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
name: 'Error', displayName: 'Error', type: 'jsonSchema', source: '=Error',
|
|
1254
|
+
body: {
|
|
1255
|
+
type: 'object',
|
|
1256
|
+
properties: {
|
|
1257
|
+
code: { type: 'string' }, message: { type: 'string' }, detail: { type: 'string' },
|
|
1258
|
+
category: { type: 'string' }, status: { type: 'number' }, element: { type: 'string' },
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
var: errorVar, id: errorVar, value: errorVar, target: `=${errorVar}`, elementId,
|
|
1262
|
+
},
|
|
1263
|
+
];
|
|
1264
|
+
}
|
|
1265
|
+
/** A minimal JSON-schema `{type:object, properties}` from library output fields. */
|
|
1266
|
+
function schemaFromFields(fields) {
|
|
1267
|
+
const properties = {};
|
|
1268
|
+
for (const f of fields) {
|
|
1269
|
+
const name = f?.name;
|
|
1270
|
+
if (!name)
|
|
1271
|
+
continue;
|
|
1272
|
+
properties[name] = { title: f.displayName ?? name, type: jsonType(f.type) };
|
|
1273
|
+
}
|
|
1274
|
+
return { type: 'object', properties };
|
|
1275
|
+
}
|
|
1276
|
+
function jsonType(t) {
|
|
1277
|
+
switch (String(t ?? '').toLowerCase()) {
|
|
1278
|
+
case 'number':
|
|
1279
|
+
case 'integer':
|
|
1280
|
+
case 'float': return 'number';
|
|
1281
|
+
case 'boolean':
|
|
1282
|
+
case 'bool': return 'boolean';
|
|
1283
|
+
case 'object': return 'object';
|
|
1284
|
+
case 'array': return 'array';
|
|
1285
|
+
default: return 'string';
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/** snake_case → camelCase (the caseplan camelCases declared param names). */
|
|
1289
|
+
function toCamel(s) {
|
|
1290
|
+
return s.replace(/_([a-z0-9])/gi, (_m, c) => String(c).toUpperCase());
|
|
1291
|
+
}
|
|
1292
|
+
// ─── Conditions & rules ───────────────────────────────────────────────────────
|
|
1293
|
+
function emitRules(rules, seed, ownerId, ids, resolver) {
|
|
1294
|
+
return rules.map((group, gi) => group.map((r, ri) => emitRule(r, `${seed}:${gi}:${ri}`, ownerId, ids, resolver)));
|
|
1295
|
+
}
|
|
1296
|
+
function emitRule(r, seed, ownerId, ids, resolver) {
|
|
1297
|
+
const ruleId = ids.ruleId(seed);
|
|
1298
|
+
const out = { rule: r.rule, id: ruleId };
|
|
1299
|
+
if (r.rule === 'selected-stage-completed' || r.rule === 'selected-stage-exited') {
|
|
1300
|
+
const sid = r.stage && resolver.stageIdByLabel.get(r.stage);
|
|
1301
|
+
if (!sid)
|
|
1302
|
+
throw new Error(`rule "${r.rule}" references unknown stage "${r.stage}".`);
|
|
1303
|
+
out[CASE_FORMAT_PROFILE.selectedStageField] = CASE_FORMAT_PROFILE.selectedStageCardinality === 'singular' ? sid : [sid];
|
|
1304
|
+
}
|
|
1305
|
+
if (r.rule === 'selected-tasks-completed') {
|
|
1306
|
+
const names = r.tasks ?? [];
|
|
1307
|
+
out.selectedTasksIds = names.map((n) => {
|
|
1308
|
+
const tid = resolver.taskIdByKey.get(n);
|
|
1309
|
+
if (!tid)
|
|
1310
|
+
throw new Error(`rule "selected-tasks-completed" references unknown task "${n}".`);
|
|
1311
|
+
return tid;
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
if (r.rule === 'wait-for-connector') {
|
|
1315
|
+
// A rule always carries the subscription under `uipath` (placeholder-safe).
|
|
1316
|
+
out[CASE_FORMAT_PROFILE.waitConnectorRuleBagField] = waitConnectorBag(r.connector, { alwaysContext: true }, resolver, `${ownerId}-${ruleId}`);
|
|
1317
|
+
}
|
|
1318
|
+
if (r.rule === 'sla-status-change') {
|
|
1319
|
+
// Resolve the SLA (and, for at-risk, the escalation) by displayName → the
|
|
1320
|
+
// preallocated id. `slaId` alone = a breach rule; adding `escalationId` = at-risk.
|
|
1321
|
+
const slaId = r.sla && resolver.slaIdByName.get(r.sla);
|
|
1322
|
+
if (!slaId)
|
|
1323
|
+
throw new Error(`rule "sla-status-change" references unknown SLA "${r.sla}" — declare it with .sla({ displayName: "${r.sla}", … }).`);
|
|
1324
|
+
out.slaId = slaId;
|
|
1325
|
+
if (r.escalation) {
|
|
1326
|
+
const escId = resolver.escalationIdByName.get(r.escalation);
|
|
1327
|
+
if (!escId)
|
|
1328
|
+
throw new Error(`rule "sla-status-change" references unknown escalation "${r.escalation}".`);
|
|
1329
|
+
out.escalationId = escId;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (r.expression)
|
|
1333
|
+
out.conditionExpression = r.expression;
|
|
1334
|
+
return out;
|
|
1335
|
+
}
|
|
1336
|
+
function emitEntry(c, seed, ownerId, label, ids, resolver) {
|
|
1337
|
+
const out = {
|
|
1338
|
+
id: ids.conditionId(seed),
|
|
1339
|
+
displayName: resolver.conditionName(c.displayName, `${label} entry`),
|
|
1340
|
+
rules: emitRules(c.rules, seed, ownerId, ids, resolver),
|
|
1341
|
+
};
|
|
1342
|
+
if (c.isInterrupting !== undefined)
|
|
1343
|
+
out.isInterrupting = c.isInterrupting;
|
|
1344
|
+
return out;
|
|
1345
|
+
}
|
|
1346
|
+
function emitExit(c, seed, ownerId, label, ids, resolver) {
|
|
1347
|
+
const out = {
|
|
1348
|
+
id: ids.conditionId(seed),
|
|
1349
|
+
displayName: resolver.conditionName(c.displayName, `${label} ${c.marksStageComplete ? 'complete' : 'exit'}`),
|
|
1350
|
+
rules: emitRules(c.rules, seed, ownerId, ids, resolver),
|
|
1351
|
+
};
|
|
1352
|
+
if (c.marksStageComplete !== undefined)
|
|
1353
|
+
out.marksStageComplete = c.marksStageComplete;
|
|
1354
|
+
if (c.type)
|
|
1355
|
+
out.type = c.type;
|
|
1356
|
+
if (c.exitToStage) {
|
|
1357
|
+
const sid = resolver.stageIdByLabel.get(c.exitToStage);
|
|
1358
|
+
if (!sid)
|
|
1359
|
+
throw new Error(`exit condition routes to unknown stage "${c.exitToStage}".`);
|
|
1360
|
+
out.exitToStageId = sid;
|
|
1361
|
+
}
|
|
1362
|
+
return out;
|
|
1363
|
+
}
|
|
1364
|
+
function emitTaskEntry(c, seed, ownerId, label, ids, resolver) {
|
|
1365
|
+
return {
|
|
1366
|
+
id: ids.conditionId(seed),
|
|
1367
|
+
displayName: resolver.conditionName(c.displayName, `${label} entry`),
|
|
1368
|
+
rules: emitRules(c.rules, seed, ownerId, ids, resolver),
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
function emitCaseExit(c, i, ids, resolver) {
|
|
1372
|
+
const seed = `case:exit:${i}`;
|
|
1373
|
+
const out = {
|
|
1374
|
+
id: ids.conditionId(seed),
|
|
1375
|
+
displayName: resolver.conditionName(c.displayName, 'Case complete'),
|
|
1376
|
+
rules: emitRules(c.rules, seed, 'root', ids, resolver),
|
|
1377
|
+
};
|
|
1378
|
+
if (c.marksCaseComplete !== undefined)
|
|
1379
|
+
out.marksCaseComplete = c.marksCaseComplete;
|
|
1380
|
+
return out;
|
|
1381
|
+
}
|
|
1382
|
+
/** One `slaRules[]` entry: the deadline + its escalations. `id` (+ `displayName`
|
|
1383
|
+
* when named) is preallocated so a `sla-status-change` rule can reference it. */
|
|
1384
|
+
function emitSla(sla, index, count, ids, resolver) {
|
|
1385
|
+
const displayName = sla.displayName ?? (index === count - 1 ? 'Default SLA' : `Conditional SLA ${count - index - 1}`);
|
|
1386
|
+
const out = {
|
|
1387
|
+
id: resolver.slaId.get(sla),
|
|
1388
|
+
...(sla.displayName || CASE_FORMAT_PROFILE.requireSlaDisplayNames ? { displayName } : {}),
|
|
1389
|
+
expression: sla.expression,
|
|
1390
|
+
count: sla.count,
|
|
1391
|
+
unit: sla.unit,
|
|
1392
|
+
};
|
|
1393
|
+
if (sla.escalations.length) {
|
|
1394
|
+
out.escalationRule = sla.escalations.map((e, escalationIndex) => emitEscalation(e, escalationIndex, displayName, resolver));
|
|
1395
|
+
}
|
|
1396
|
+
return out;
|
|
1397
|
+
}
|
|
1398
|
+
/** One escalation: a trigger (at-risk % or breach) and a notification action. */
|
|
1399
|
+
function emitEscalation(e, index, slaDisplayName, resolver) {
|
|
1400
|
+
const triggerInfo = { type: e.trigger };
|
|
1401
|
+
if (e.trigger === 'at-risk')
|
|
1402
|
+
triggerInfo.atRiskPercentage = e.atRiskPercentage ?? 100;
|
|
1403
|
+
return {
|
|
1404
|
+
id: resolver.escalationId.get(e),
|
|
1405
|
+
...(e.displayName || CASE_FORMAT_PROFILE.requireSlaDisplayNames
|
|
1406
|
+
? { displayName: e.displayName ?? `Escalation rule ${index + 1} - ${slaDisplayName}` }
|
|
1407
|
+
: {}),
|
|
1408
|
+
triggerInfo,
|
|
1409
|
+
action: {
|
|
1410
|
+
type: 'notification',
|
|
1411
|
+
recipients: e.notify.map((r) => ({ scope: r.scope, target: r.target, value: r.value ?? r.target })),
|
|
1412
|
+
},
|
|
1413
|
+
};
|
|
1414
|
+
}
|