@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.
Files changed (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/dist/bindings-v2.d.ts +37 -0
  4. package/dist/bindings-v2.js +155 -0
  5. package/dist/bindings.d.ts +6 -0
  6. package/dist/bindings.js +6 -0
  7. package/dist/bpmn/bpmn-expr-check.d.ts +41 -0
  8. package/dist/bpmn/bpmn-expr-check.js +218 -0
  9. package/dist/bpmn/bpmn-sdk.d.ts +2257 -0
  10. package/dist/bpmn/bpmn-sdk.js +1476 -0
  11. package/dist/bpmn/check-cli.d.ts +3 -0
  12. package/dist/bpmn/check-cli.js +32 -0
  13. package/dist/bpmn/check.d.ts +25 -0
  14. package/dist/bpmn/check.js +430 -0
  15. package/dist/bpmn/compile-cli.d.ts +3 -0
  16. package/dist/bpmn/compile-cli.js +60 -0
  17. package/dist/bpmn/connector.d.ts +56 -0
  18. package/dist/bpmn/connector.js +199 -0
  19. package/dist/bpmn/decompile-cli.d.ts +8 -0
  20. package/dist/bpmn/decompile-cli.js +54 -0
  21. package/dist/bpmn/decompile.d.ts +6 -0
  22. package/dist/bpmn/decompile.js +1108 -0
  23. package/dist/bpmn/format-profile.d.ts +27 -0
  24. package/dist/bpmn/format-profile.js +11 -0
  25. package/dist/bpmn/intsvc.d.ts +77 -0
  26. package/dist/bpmn/intsvc.js +155 -0
  27. package/dist/bpmn/load.d.ts +4 -0
  28. package/dist/bpmn/load.js +22 -0
  29. package/dist/bpmn/merge-cli.d.ts +3 -0
  30. package/dist/bpmn/merge-cli.js +45 -0
  31. package/dist/bpmn/merge.d.ts +19 -0
  32. package/dist/bpmn/merge.js +293 -0
  33. package/dist/bpmn/registry-types.json +1869 -0
  34. package/dist/bpmn/serialize.d.ts +28 -0
  35. package/dist/bpmn/serialize.js +757 -0
  36. package/dist/bpmn/tidy-cli.d.ts +2 -0
  37. package/dist/bpmn/tidy-cli.js +39 -0
  38. package/dist/bpmn/typed-node.d.ts +382 -0
  39. package/dist/bpmn/typed-node.js +551 -0
  40. package/dist/bpmn/uipath-moddle.v1.json +823 -0
  41. package/dist/bpmn/xml-ranges.d.ts +45 -0
  42. package/dist/bpmn/xml-ranges.js +151 -0
  43. package/dist/case/case-expr-check.d.ts +35 -0
  44. package/dist/case/case-expr-check.js +85 -0
  45. package/dist/case/case-sdk.d.ts +1282 -0
  46. package/dist/case/case-sdk.js +923 -0
  47. package/dist/case/check-cli.d.ts +3 -0
  48. package/dist/case/check-cli.js +31 -0
  49. package/dist/case/check.d.ts +18 -0
  50. package/dist/case/check.js +456 -0
  51. package/dist/case/compile-cli.d.ts +3 -0
  52. package/dist/case/compile-cli.js +291 -0
  53. package/dist/case/decompile-cli.d.ts +9 -0
  54. package/dist/case/decompile-cli.js +117 -0
  55. package/dist/case/decompile.d.ts +187 -0
  56. package/dist/case/decompile.js +1170 -0
  57. package/dist/case/entry-points.d.ts +79 -0
  58. package/dist/case/entry-points.js +118 -0
  59. package/dist/case/format-profile.d.ts +60 -0
  60. package/dist/case/format-profile.js +79 -0
  61. package/dist/case/ids.d.ts +24 -0
  62. package/dist/case/ids.js +76 -0
  63. package/dist/case/load.d.ts +12 -0
  64. package/dist/case/load.js +32 -0
  65. package/dist/case/preserve.d.ts +133 -0
  66. package/dist/case/preserve.js +105 -0
  67. package/dist/case/serialize.d.ts +10 -0
  68. package/dist/case/serialize.js +1414 -0
  69. package/dist/check-cli.d.ts +3 -0
  70. package/dist/check-cli.js +118 -0
  71. package/dist/check.d.ts +146 -0
  72. package/dist/check.js +4898 -0
  73. package/dist/cli/commands/bpmn.d.ts +2 -0
  74. package/dist/cli/commands/bpmn.js +73 -0
  75. package/dist/cli/commands/case.d.ts +2 -0
  76. package/dist/cli/commands/case.js +60 -0
  77. package/dist/cli/commands/flow.d.ts +2 -0
  78. package/dist/cli/commands/flow.js +99 -0
  79. package/dist/cli/commands/registry.d.ts +10 -0
  80. package/dist/cli/commands/registry.js +170 -0
  81. package/dist/cli/common.d.ts +30 -0
  82. package/dist/cli/common.js +92 -0
  83. package/dist/cli/index.d.ts +2 -0
  84. package/dist/cli/index.js +18 -0
  85. package/dist/cli-run.d.ts +2 -0
  86. package/dist/cli-run.js +42 -0
  87. package/dist/compile-cli.d.ts +3 -0
  88. package/dist/compile-cli.js +160 -0
  89. package/dist/config.d.ts +47 -0
  90. package/dist/config.js +201 -0
  91. package/dist/core/actions.d.ts +3244 -0
  92. package/dist/core/actions.js +1062 -0
  93. package/dist/core/binding-messages.d.ts +1 -0
  94. package/dist/core/binding-messages.js +16 -0
  95. package/dist/core/bindings.d.ts +12 -0
  96. package/dist/core/bindings.js +67 -0
  97. package/dist/core/ceql.d.ts +73 -0
  98. package/dist/core/ceql.js +272 -0
  99. package/dist/core/cli-spelling.d.ts +39 -0
  100. package/dist/core/cli-spelling.js +62 -0
  101. package/dist/core/connector-raw-node.d.ts +21 -0
  102. package/dist/core/connector-raw-node.js +47 -0
  103. package/dist/core/connectors.d.ts +184 -0
  104. package/dist/core/connectors.js +128 -0
  105. package/dist/core/expr-check.d.ts +93 -0
  106. package/dist/core/expr-check.js +186 -0
  107. package/dist/core/expr.d.ts +323 -0
  108. package/dist/core/expr.js +400 -0
  109. package/dist/core/index.d.ts +10 -0
  110. package/dist/core/index.js +10 -0
  111. package/dist/core/library.d.ts +440 -0
  112. package/dist/core/library.js +1183 -0
  113. package/dist/core/list-envelope.d.ts +45 -0
  114. package/dist/core/list-envelope.js +97 -0
  115. package/dist/core/lookup-store.d.ts +22 -0
  116. package/dist/core/lookup-store.js +85 -0
  117. package/dist/core/lookups.d.ts +227 -0
  118. package/dist/core/lookups.js +231 -0
  119. package/dist/core/node-classes.d.ts +234 -0
  120. package/dist/core/node-classes.js +158 -0
  121. package/dist/core/script-return.d.ts +68 -0
  122. package/dist/core/script-return.js +197 -0
  123. package/dist/core/stable-id.d.ts +28 -0
  124. package/dist/core/stable-id.js +37 -0
  125. package/dist/core-definitions.json +16249 -0
  126. package/dist/decompile-cli.d.ts +3 -0
  127. package/dist/decompile-cli.js +131 -0
  128. package/dist/decompile.d.ts +138 -0
  129. package/dist/decompile.js +2482 -0
  130. package/dist/emit.d.ts +10 -0
  131. package/dist/emit.js +60 -0
  132. package/dist/event-filters.d.ts +130 -0
  133. package/dist/event-filters.js +137 -0
  134. package/dist/flow-expr-check.d.ts +17 -0
  135. package/dist/flow-expr-check.js +193 -0
  136. package/dist/flow-sdk.d.ts +1341 -0
  137. package/dist/flow-sdk.js +1164 -0
  138. package/dist/flow-variables.d.ts +13 -0
  139. package/dist/flow-variables.js +42 -0
  140. package/dist/format-profile.d.ts +72 -0
  141. package/dist/format-profile.js +97 -0
  142. package/dist/generators/_connections.py +287 -0
  143. package/dist/generators/_events.py +355 -0
  144. package/dist/generators/_lookups.py +114 -0
  145. package/dist/generators/_naming.py +185 -0
  146. package/dist/generators/_objects.py +299 -0
  147. package/dist/generators/_resolve.py +273 -0
  148. package/dist/generators/convert_library_to_md.py +1441 -0
  149. package/dist/generators/extract_library.py +125 -0
  150. package/dist/generators/generate_connectors_ts.py +936 -0
  151. package/dist/generators/generate_library_json.py +2527 -0
  152. package/dist/generators/generate_triggers_ts.py +110 -0
  153. package/dist/generators/materialize_registry.py +155 -0
  154. package/dist/generators/prepare_connector.py +1688 -0
  155. package/dist/index.d.ts +35 -0
  156. package/dist/index.js +35 -0
  157. package/dist/ixp-definition.json +1982 -0
  158. package/dist/library.d.ts +6 -0
  159. package/dist/library.js +6 -0
  160. package/dist/load.d.ts +16 -0
  161. package/dist/load.js +95 -0
  162. package/dist/merge-cli.d.ts +3 -0
  163. package/dist/merge-cli.js +47 -0
  164. package/dist/merge.d.ts +125 -0
  165. package/dist/merge.js +0 -0
  166. package/dist/migrate.d.ts +15 -0
  167. package/dist/migrate.js +245 -0
  168. package/dist/node-runtime.d.ts +3 -0
  169. package/dist/node-runtime.js +22 -0
  170. package/dist/package-name.d.ts +17 -0
  171. package/dist/package-name.js +17 -0
  172. package/dist/ref-resolve.d.ts +77 -0
  173. package/dist/ref-resolve.js +170 -0
  174. package/dist/registry/cache.d.ts +55 -0
  175. package/dist/registry/cache.js +134 -0
  176. package/dist/registry/curated-overlay.d.ts +16 -0
  177. package/dist/registry/curated-overlay.js +62 -0
  178. package/dist/registry/extractor.d.ts +22 -0
  179. package/dist/registry/extractor.js +18 -0
  180. package/dist/registry/materialize-bin.d.ts +2 -0
  181. package/dist/registry/materialize-bin.js +7 -0
  182. package/dist/registry/materialize-cli.d.ts +1 -0
  183. package/dist/registry/materialize-cli.js +46 -0
  184. package/dist/registry/materializer.d.ts +23 -0
  185. package/dist/registry/materializer.js +34 -0
  186. package/dist/registry/path-cli.d.ts +1 -0
  187. package/dist/registry/path-cli.js +56 -0
  188. package/dist/registry/prepare-cli.d.ts +62 -0
  189. package/dist/registry/prepare-cli.js +185 -0
  190. package/dist/registry/pull-cli.d.ts +1 -0
  191. package/dist/registry/pull-cli.js +274 -0
  192. package/dist/registry/python.d.ts +26 -0
  193. package/dist/registry/python.js +46 -0
  194. package/dist/registry/search-cli.d.ts +52 -0
  195. package/dist/registry/search-cli.js +159 -0
  196. package/dist/schema-version.d.ts +165 -0
  197. package/dist/schema-version.js +148 -0
  198. package/dist/serialize.d.ts +64 -0
  199. package/dist/serialize.js +6398 -0
  200. package/lib/README.md +112 -0
  201. package/lib/library-json.samples/index.json +158 -0
  202. package/lib/library-json.samples/invariant-collections.json +83 -0
  203. package/lib/library-json.samples/list-envelopes.json +123 -0
  204. package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.json +131 -0
  205. package/lib/library-json.samples/uipath-atlassian-jira/create-issue@1.0.0.v1def.json +306 -0
  206. package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.json +86 -0
  207. package/lib/library-json.samples/uipath-atlassian-jira/generic/create_issue/create@1.0.0.v1def.json +208 -0
  208. package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.json +2040 -0
  209. package/lib/library-json.samples/uipath-atlassian-jira/get-issue@1.0.0.v1def.json +195 -0
  210. package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.json +169 -0
  211. package/lib/library-json.samples/uipath-google-vertex/execute-google-vertex-agent@1.0.0.v1def.json +209 -0
  212. package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.json +63 -0
  213. package/lib/library-json.samples/uipath-http-webhook/event-http-webhook@1.0.0.v1def.json +262 -0
  214. package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.json +63 -0
  215. package/lib/library-json.samples/uipath-http-webhook/trigger-http-webhook@1.0.0.v1def.json +213 -0
  216. package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.json +227 -0
  217. package/lib/library-json.samples/uipath-microsoft-azureactivedirectory/list-groups@1.0.0.v1def.json +373 -0
  218. package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.json +116 -0
  219. package/lib/library-json.samples/uipath-microsoft-onedrive/event-file-created@1.0.0.v1def.json +369 -0
  220. package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.json +56 -0
  221. package/lib/library-json.samples/uipath-microsoft-onedrive/event-list-item-added@1.0.0.v1def.json +256 -0
  222. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.json +116 -0
  223. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-file-created@1.0.0.v1def.json +320 -0
  224. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.json +56 -0
  225. package/lib/library-json.samples/uipath-microsoft-onedrive/trigger-list-item-added@1.0.0.v1def.json +207 -0
  226. package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.json +107 -0
  227. package/lib/library-json.samples/uipath-microsoft-outlook365/event-email-received@1.0.0.v1def.json +264 -0
  228. package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.json +142 -0
  229. package/lib/library-json.samples/uipath-microsoft-outlook365/send-email@1.0.0.v1def.json +306 -0
  230. package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.json +107 -0
  231. package/lib/library-json.samples/uipath-microsoft-outlook365/trigger-email-received@1.0.0.v1def.json +217 -0
  232. package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.json +85 -0
  233. package/lib/library-json.samples/uipath-microsoft-powerautomate/invoke-a-microsoft-power-automate-flow@1.0.0.v1def.json +209 -0
  234. package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.json +755 -0
  235. package/lib/library-json.samples/uipath-salesforce-slack/send-message-to-user@1.0.0.v1def.json +513 -0
  236. package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.json +122 -0
  237. package/lib/library-json.samples/uipath-uipath-dataservice/query-entity-records@1.0.0.v1def.json +227 -0
  238. package/package.json +85 -0
@@ -0,0 +1,936 @@
1
+ #!/usr/bin/env python3
2
+ """Generate statically-typed TypeScript connector descriptors from the canonical
3
+ JSON connector library.
4
+
5
+ For every **Curated** and **HTTP-request** op, emit one `descriptor<Inputs,
6
+ Outputs>()` const plus its `Inputs`/`Outputs` interfaces, grouped one module per
7
+ connector:
8
+
9
+ connectors/<connector-key>.ts
10
+ export interface FieldsInput { project: ProjectInput; summary: string; … }
11
+ export interface ProjectInput { key: string; }
12
+ export interface CreateIssueInputs { fields: FieldsInput; }
13
+ export interface Fields { issuetype?: Issuetype; }
14
+ export interface Issuetype { name?: string; }
15
+ export interface CreateIssueOutputs { fields?: Fields; id?: string; key?: string; … }
16
+ export const CreateIssue = descriptor<CreateIssueInputs, CreateIssueOutputs>()({
17
+ nodeType: 'uipath.connector.uipath-atlassian-jira.create-issue',
18
+ version: '1.0.0', requiresConnection: true, requiresFolderKey: true,
19
+ } as const);
20
+
21
+ Inputs are the union of the op's **v1def.inputDefinition.fields** (body) and
22
+ **entry.operation.parameters** (query/path/multipart). When both declarations are empty
23
+ but the catalog still declares required inputs, **entry.inputSchema.fields** is
24
+ the only available authoring surface and is used as a fallback. Catalog file
25
+ fields also fill older snapshots that omitted their multipart parameter. Dotted
26
+ and wildcard body fields are reconstructed as natural nested objects/arrays; `serialize()`/
27
+ `buildConnectorInputs` project them back to the exact registry field ids and
28
+ route every field to its declared Flow JSON slot. Outputs come from the entry's
29
+ `outputSchema.fields` and use the same nested reusable-type model.
30
+
31
+ **GenericExpanded ops are skipped for the bulk static library by default** to
32
+ avoid emitting thousands of descriptors. A small connection-resolved overlay
33
+ passes `--include-generic-expanded`; those entries carry the selected object's
34
+ live fields in `v1def.inputDefinition.fields`, and their descriptors bake in
35
+ `ConnectorMeta.objectName` so authoring needs no separate `{ object }` option.
36
+
37
+ Usage:
38
+ generate_connectors_ts.py --library <json-lib-dir> --output <ts-dir>
39
+ [--import <specifier>] [--connectors a,b,c]
40
+ [--include-generic-expanded]
41
+ """
42
+ from __future__ import annotations
43
+
44
+ import argparse
45
+ import json
46
+ import re
47
+ import sys
48
+ from pathlib import Path
49
+
50
+ # Symbol derivation is SHARED with convert_library_to_md.py — the markdown cites
51
+ # these names, and a name derived twice is a name that drifts. See _naming.py.
52
+ from _naming import by_aliases, pascal, singular, symbol_index
53
+ from _lookups import classify, load_invariant_collections, reference_of
54
+
55
+ TYPE_MAP = {
56
+ "string": "string", "number": "number", "integer": "number", "float": "number",
57
+ "double": "number", "long": "number",
58
+ "boolean": "boolean", "bool": "boolean", "object": "Record<string, unknown>",
59
+ "array": "unknown[]", "json": "unknown", "file": "string",
60
+ }
61
+
62
+
63
+ def is_container_part(parameter: dict) -> bool:
64
+ """A multipart parameter that is not a file.
65
+
66
+ The runtime composes it from ``bodyParameters`` (Teams ``body`` from
67
+ ``body.content``, Gmail ``body`` from ``Body``, GenAI ``RagRequest`` from
68
+ ``prompt``); the author never fills it, and a value written into it is
69
+ ignored at dispatch (verified live 2026-09-02). It is listed in the emitted
70
+ ``multipartParameters`` by the compiler, not offered as an input here — the
71
+ same rule as ``core/library.ts``'s ``isContainerPart``.
72
+ """
73
+ return (
74
+ parameter.get("type") == "multipart"
75
+ and str(parameter.get("dataType") or "string").lower() != "file"
76
+ )
77
+
78
+
79
+ def authoritative_input_fields(entry: dict, v1def: dict) -> list[dict]:
80
+ """Return the same complete input vocabulary consumed by the SDK runtime.
81
+
82
+ Body fields live in ``v1def.inputDefinition.fields``; query, path, and
83
+ FILE multipart parameters live in ``entry.operation.parameters``. A
84
+ declaration wins when the same name appears in both because its wire
85
+ location is authoritative. A string-typed multipart parameter is the
86
+ transport container, not an input (see ``is_container_part``).
87
+ """
88
+ fields = [dict(field) for field in
89
+ (((v1def.get("inputDefinition") or {}).get("fields")) or [])]
90
+ positions = {field.get("name"): index for index, field in enumerate(fields)}
91
+ parameters = ((entry.get("operation") or {}).get("parameters")) or []
92
+ for parameter in parameters:
93
+ name = parameter.get("name")
94
+ if not name or parameter.get("type") not in {
95
+ "query", "path", "multipart"
96
+ }:
97
+ continue
98
+ if is_container_part(parameter):
99
+ continue
100
+ field = {
101
+ "name": name,
102
+ "type": parameter.get("dataType"),
103
+ "required": parameter.get("required") is True,
104
+ }
105
+ if parameter.get("displayName"):
106
+ field["displayName"] = parameter["displayName"]
107
+ if parameter.get("description"):
108
+ field["description"] = parameter["description"]
109
+ # A QUERY parameter can carry a `reference` too — Slack's `send_as`
110
+ # does. Rebuilding the field from scratch dropped it, which made those
111
+ # lookups invisible to everything downstream.
112
+ if isinstance(parameter.get("reference"), dict):
113
+ field["reference"] = parameter["reference"]
114
+ if name in positions:
115
+ fields[positions[name]] = field
116
+ else:
117
+ positions[name] = len(fields)
118
+ fields.append(field)
119
+ return fields
120
+
121
+
122
+ def input_fields(entry: dict, v1def: dict) -> list[dict]:
123
+ fields = authoritative_input_fields(entry, v1def)
124
+ catalog_fields = (entry.get("inputSchema") or {}).get("fields") or []
125
+ if not fields and any(
126
+ field.get("required") is True for field in catalog_fields
127
+ ):
128
+ fields = [dict(field) for field in catalog_fields]
129
+ positions = {field.get("name"): index for index, field in enumerate(fields)}
130
+ for catalog_field in catalog_fields:
131
+ name = catalog_field.get("name")
132
+ if (
133
+ not name
134
+ or str(catalog_field.get("type") or "").lower() != "file"
135
+ or name in positions
136
+ ):
137
+ continue
138
+ positions[name] = len(fields)
139
+ fields.append(dict(catalog_field))
140
+ return fields
141
+
142
+
143
+ EVENT_NODE_PREFIXES = ("uipath.connector.event.", "uipath.connector.trigger.")
144
+
145
+
146
+ def is_event_node(node_type: str) -> bool:
147
+ """An event/trigger entry, as opposed to a connector ACTION."""
148
+ return str(node_type or "").startswith(EVENT_NODE_PREFIXES)
149
+
150
+
151
+ def descriptor_input_fields(entry: dict, v1def: dict) -> list[dict]:
152
+ """The one field list a descriptor's inputs — or an event's `where` — come from.
153
+
154
+ An event's authored inputs are its SUBSCRIPTION PARAMETERS, and
155
+ `v1def.eventParameters.fields` is the only place that holds them on BOTH
156
+ library shapes. A published entry happens to repeat them in
157
+ `inputDefinition.fields`, so reading `input_fields()` for an event looked
158
+ right; the overlay `prepare` writes leaves `inputDefinition.fields` empty
159
+ and keeps the parameters in the sidecar alone. The emit path read
160
+ `input_fields()` while the loop that builds the type tree read
161
+ `eventParameters` — two rules for one list — so after a `prepare`, and only
162
+ after a `prepare`, `where` degraded to `Record<string, string>` and the
163
+ descriptor carried no `lookups`. That is the descriptor `prepare` tells the
164
+ author to import, so `lookup(EmailReceived, 'parentFolderId')
165
+ .byDisplayName('Inbox')` failed with a bare TypeError on exactly the path
166
+ the skill steers them down.
167
+
168
+ An entry that declares none keeps the loose `where` (see `emit_event`).
169
+ """
170
+ if is_event_node(entry.get("nodeType") or ""):
171
+ return (v1def.get("eventParameters") or {}).get("fields") or []
172
+ return input_fields(entry, v1def)
173
+
174
+
175
+ def required_input_gap(
176
+ entry: dict, input_tree: dict[str, "TypeNode"]
177
+ ) -> str | None:
178
+ required = [
179
+ str(field.get("name") or "<unnamed>")
180
+ for field in (entry.get("inputSchema") or {}).get("fields") or []
181
+ if field.get("required") is True
182
+ ]
183
+ if not required or input_tree:
184
+ return None
185
+ return (
186
+ f'{entry.get("nodeType", "<unknown node>")}: catalog requires '
187
+ f'{", ".join(required)}, but the generated inputs interface is empty'
188
+ )
189
+
190
+
191
+ def ts_type(field: dict) -> str:
192
+ return TYPE_MAP.get(str(field.get("type") or "").lower(), "unknown")
193
+
194
+
195
+ class TypeNode:
196
+ """One node in a tree reconstructed from flattened connector field paths."""
197
+
198
+ def __init__(self, *, is_array: bool = False) -> None:
199
+ self.array_modes: set[bool] = {is_array}
200
+ self.declared_types: set[str] = set()
201
+ self.declared: dict | None = None
202
+ self.opaque_object = False
203
+ self.opaque_array = False
204
+ self.required = False
205
+ self.children: dict[str, TypeNode] = {}
206
+
207
+ def add_declaration(self, field: dict) -> None:
208
+ if self.declared is None:
209
+ self.declared = field
210
+ declared_type = str(field.get("type") or "").lower()
211
+ self.declared_types.add(ts_type(field))
212
+ self.opaque_object = self.opaque_object or declared_type == "object"
213
+ self.opaque_array = self.opaque_array or declared_type == "array"
214
+ self.required = self.required or bool(field.get("required"))
215
+
216
+
217
+ def split_field_path(name: str) -> list[tuple[str, bool]]:
218
+ """Split ``items[*].owner.id`` into segments plus array cardinality."""
219
+ parts: list[tuple[str, bool]] = []
220
+ for raw in name.split("."):
221
+ is_array = raw.endswith("[*]")
222
+ segment = raw[:-3] if is_array else raw
223
+ # Preserve malformed/opaque registry names as one quoted property.
224
+ if not segment:
225
+ return [(name, False)]
226
+ parts.append((segment, is_array))
227
+ return parts
228
+
229
+
230
+ def build_type_tree(fields: list[dict]) -> dict[str, TypeNode]:
231
+ roots: dict[str, TypeNode] = {}
232
+ for field in fields:
233
+ name = field.get("name")
234
+ if not name:
235
+ continue
236
+ cursor = roots
237
+ node: TypeNode | None = None
238
+ for segment, is_array in split_field_path(str(name)):
239
+ node = cursor.setdefault(segment, TypeNode(is_array=is_array))
240
+ node.array_modes.add(is_array)
241
+ cursor = node.children
242
+ if node is not None:
243
+ node.add_declaration(field)
244
+ return roots
245
+
246
+
247
+ def merge_type_nodes(target: TypeNode, source: TypeNode) -> None:
248
+ """Merge repeated occurrences into one reusable connector-level type."""
249
+ target.array_modes.update(source.array_modes)
250
+ target.declared_types.update(source.declared_types)
251
+ target.opaque_object = target.opaque_object or source.opaque_object
252
+ target.opaque_array = target.opaque_array or source.opaque_array
253
+ target.required = target.required or source.required
254
+ if target.declared is None:
255
+ target.declared = source.declared
256
+ for child_name, source_child in source.children.items():
257
+ target_child = target.children.get(child_name)
258
+ if target_child is None:
259
+ target.children[child_name] = source_child
260
+ else:
261
+ merge_type_nodes(target_child, source_child)
262
+
263
+
264
+ def nested_type_name(field_name: str, node: TypeNode, suffix: str = "") -> str:
265
+ source = singular(field_name) if (True in node.array_modes or node.opaque_array) else field_name
266
+ name = pascal(source)
267
+ name = name if re.match(r"[A-Za-z_$]", name) else f"Nested{name}"
268
+ return f"{name}{suffix}"
269
+
270
+
271
+ def key_literal(name: str) -> str:
272
+ # A field name is a valid ident only if it has no dots/brackets/etc.
273
+ return name if re.fullmatch(r"[A-Za-z_$][A-Za-z0-9_$]*", name) else json.dumps(name)
274
+
275
+
276
+ def subtree_required(node: TypeNode) -> bool:
277
+ return node.required or any(subtree_required(child) for child in node.children.values())
278
+
279
+
280
+ def type_fingerprint(node: TypeNode) -> tuple:
281
+ """Stable structural identity; input types reuse only exact shapes."""
282
+ return (
283
+ tuple(sorted(node.array_modes)),
284
+ tuple(sorted(node.declared_types)),
285
+ node.opaque_object,
286
+ node.opaque_array,
287
+ node.required,
288
+ tuple((name, type_fingerprint(child)) for name, child in sorted(node.children.items())),
289
+ )
290
+
291
+
292
+ class NamedTypeRegistry:
293
+ """Reusable named interfaces shared by all ops in one connector."""
294
+
295
+ def __init__(
296
+ self, *, suffix: str = "", all_optional: bool = True, merge_reused: bool = True,
297
+ is_input: bool = False,
298
+ ) -> None:
299
+ self.is_input = is_input
300
+ self.suffix = suffix
301
+ self.all_optional = all_optional
302
+ self.merge_reused = merge_reused
303
+ self._by_name: dict[str, TypeNode] = {}
304
+ self._by_node: dict[int, str] = {}
305
+ self._shape_names: dict[tuple[str, tuple], str] = {}
306
+ self._definitions: list[str] = []
307
+ self._registering: set[str] = set()
308
+
309
+ def register(self, field_name: str, node: TypeNode) -> str:
310
+ known = self._by_node.get(id(node))
311
+ if known:
312
+ return known
313
+
314
+ base_name = nested_type_name(field_name, node, self.suffix)
315
+ if self.merge_reused:
316
+ type_name = base_name
317
+ else:
318
+ shape_key = (base_name, type_fingerprint(node))
319
+ type_name = self._shape_names.get(shape_key, "")
320
+ if not type_name:
321
+ type_name = base_name
322
+ suffix = 2
323
+ while type_name in self._by_name:
324
+ type_name = f"{base_name}{suffix}"
325
+ suffix += 1
326
+ self._shape_names[shape_key] = type_name
327
+
328
+ canonical = self._by_name.get(type_name)
329
+ if canonical is None:
330
+ canonical = node
331
+ self._by_name[type_name] = canonical
332
+ self._definitions.append(type_name)
333
+ elif canonical is not node:
334
+ if self.merge_reused:
335
+ merge_type_nodes(canonical, node)
336
+ self._by_node[id(node)] = type_name
337
+
338
+ # A schema can recursively repeat the same segment name
339
+ # (`children[*].children[*]...`). Merge it into the shared interface,
340
+ # but do not recursively rediscover that interface while it is active.
341
+ if type_name in self._registering:
342
+ return type_name
343
+ self._registering.add(type_name)
344
+
345
+ # Registration can enrich a canonical node, so iterate a snapshot.
346
+ try:
347
+ for child_name, child in list(canonical.children.items()):
348
+ if child.children or child.opaque_object:
349
+ self.register(child_name, child)
350
+ finally:
351
+ self._registering.remove(type_name)
352
+ return type_name
353
+
354
+ def discover(self, roots: dict[str, TypeNode]) -> None:
355
+ for field_name, node in roots.items():
356
+ if node.children or node.opaque_object:
357
+ self.register(field_name, node)
358
+
359
+ def _known_array_element(self, field_name: str, node: TypeNode) -> str | None:
360
+ candidate = nested_type_name(field_name, node, self.suffix)
361
+ matches = [
362
+ name for name in self._by_name
363
+ if name == candidate or re.fullmatch(re.escape(candidate) + r"[2-9][0-9]*", name)
364
+ ]
365
+ return matches[0] if len(matches) == 1 else None
366
+
367
+ def type_for(self, field_name: str, node: TypeNode) -> str:
368
+ if node.children or node.opaque_object:
369
+ base = self.register(field_name, node)
370
+ elif node.opaque_array:
371
+ known_element = self._known_array_element(field_name, node)
372
+ if known_element:
373
+ return f"Array<{known_element}>"
374
+ return "unknown[]"
375
+ else:
376
+ base = " | ".join(sorted(node.declared_types)) or "unknown"
377
+
378
+ has_array = True in node.array_modes
379
+ has_scalar = False in node.array_modes
380
+ if has_array and has_scalar:
381
+ return f"{base} | Array<{base}>"
382
+ if has_array:
383
+ return f"Array<{base}>"
384
+ return base
385
+
386
+ def emit(self) -> list[str]:
387
+ lines: list[str] = []
388
+ # Rendering a definition may register an opaque nested object. Iterate
389
+ # by index so newly discovered definitions are emitted too.
390
+ index = 0
391
+ while index < len(self._definitions):
392
+ type_name = self._definitions[index]
393
+ node = self._by_name[type_name]
394
+ lines.append(f"export interface {type_name} {{")
395
+ if node.opaque_object:
396
+ lines.append(" [key: string]: unknown;")
397
+ for child_name, child in node.children.items():
398
+ lines += emit_nested_property(
399
+ child_name, child, " ", self, self.all_optional, self.is_input,
400
+ )
401
+ lines.append("}")
402
+ lines.append("")
403
+ index += 1
404
+ return lines
405
+
406
+
407
+ def emit_nested_property(
408
+ name: str,
409
+ node: TypeNode,
410
+ indent: str,
411
+ named_types: NamedTypeRegistry,
412
+ all_optional: bool,
413
+ is_input: bool = False,
414
+ ) -> list[str]:
415
+ """One property line.
416
+
417
+ An INPUT property is wrapped so it also accepts an expression, and a
418
+ resolvable lookup additionally accepts an unresolved token. The wrapper goes
419
+ HERE rather than on the leaf type so that `tags: input('tags')` — the whole
420
+ collection as one expression — is legal alongside `tags: ['a', out(…)]`;
421
+ `ConnectorValue` distributes over arrays to cover both.
422
+
423
+ An OUTPUT property is never wrapped. It is a value that came back from the
424
+ connector, so it cannot be an expression or an unresolved lookup.
425
+ """
426
+ lines: list[str] = []
427
+ declared = node.declared or {}
428
+ desc = str(declared.get("description") or "").strip().replace("\n", " ")
429
+ if is_input and declared.get("__lookup"):
430
+ # The identity goes ON the field, not only in the LOOKUP block above the
431
+ # descriptor: the field is where the value gets typed, and "The reporter
432
+ # of the issue" beside `ConnectorLookupValue<string>` read as "any string"
433
+ # — an e-mail went into `fields.reporter.id` and Jira refused it at run time.
434
+ desc = f"{desc} {lookup_field_note(declared)}".strip()
435
+ if desc:
436
+ lines.append(f"{indent}/** {desc} */")
437
+ optional = "?" if all_optional or not subtree_required(node) else ""
438
+ rendered = named_types.type_for(name, node)
439
+ if is_input:
440
+ wrapper = "ConnectorLookupValue" if declared.get("__lookup") else "ConnectorValue"
441
+ rendered = f"{wrapper}<{rendered}>"
442
+ lines.append(f"{indent}{key_literal(name)}{optional}: {rendered};")
443
+ return lines
444
+
445
+
446
+ def lookup_field_note(declared: dict) -> str:
447
+ """One sentence for a lookup field's own doc comment: what it sends, how to get one."""
448
+ reference = reference_of(declared) or {}
449
+ target = str(reference.get("objectName") or str(reference.get("path") or "").split("?", 1)[0])
450
+ value = str(reference.get("lookupValue") or "id")
451
+ field = str(declared.get("name") or "")
452
+ return (
453
+ f"LOOKUP — sends a `{target}.{value}`, never a name or e-mail: "
454
+ f"lookup(<Descriptor>, '{field}') (helpers in the LOOKUP lines above the descriptor)."
455
+ )
456
+
457
+
458
+ def emit_interface(
459
+ name: str,
460
+ fields: list[dict],
461
+ all_optional: bool,
462
+ named_types: NamedTypeRegistry,
463
+ field_tree: dict[str, TypeNode],
464
+ is_input: bool = False,
465
+ ) -> list[str]:
466
+ out = [f"export interface {name} {{"]
467
+ for field_name, node in field_tree.items():
468
+ out += emit_nested_property(
469
+ field_name, node, " ", named_types, all_optional, is_input,
470
+ )
471
+ out.append("}")
472
+ return out
473
+
474
+
475
+
476
+ def lookup_strategy(reference: dict) -> str:
477
+ """Which retrieval strategy `prepare` will use for this field.
478
+
479
+ Order matters. `dependsOn` wins outright because it is a PREREQUISITE
480
+ rather than an optimisation — the collection is not addressable at all
481
+ until another field resolves, so no amount of filtering helps. After that
482
+ a server-side filter beats a tree walk beats a full scan.
483
+ """
484
+ if reference.get("dependsOn"):
485
+ return "dependent"
486
+ if reference.get("filterPattern"):
487
+ return "filter"
488
+ if reference.get("childPath"):
489
+ return "tree"
490
+ return "scan"
491
+
492
+
493
+ def lookup_specs(entry: dict, v1def: dict, invariant: set) -> dict:
494
+ """`{fieldName: spec}` for every field that earns a `.by*` helper.
495
+
496
+ Keyed by the WIRE field name — the same string `buildConnectorInputs`
497
+ matches on and the same one `prepare --resolve <field>:…` takes — so a
498
+ nested field like `fields.project.key` is addressed by its dotted path and
499
+ there is exactly one spelling of a field across the whole toolchain.
500
+
501
+ `reference` is unioned in from `inputSchema.fields` where the authoring
502
+ vocabulary lacks it: the two declarations disagree about which fields they
503
+ carry, and a lookup declared only in the catalog is still a lookup.
504
+ """
505
+ catalog_references = {
506
+ f.get("name"): f["reference"]
507
+ for f in ((entry.get("inputSchema") or {}).get("fields") or [])
508
+ if isinstance(f.get("reference"), dict)
509
+ }
510
+ return specs_from_fields(
511
+ input_fields(entry, v1def), invariant, catalog_references)
512
+
513
+
514
+ def specs_from_fields(
515
+ fields: list[dict], invariant: set, catalog_references: dict | None = None,
516
+ ) -> dict:
517
+ """`{fieldName: spec}` for the lookup-bearing fields of ANY field list.
518
+
519
+ Split out of `lookup_specs` so a connector EVENT can build the same specs
520
+ from its `eventParameters.fields` — the list `lookup()` resolves against for
521
+ a subscription — instead of from `inputSchema`/`inputDefinition`, which an
522
+ event does not use. Two emitters, one implementation: when they were
523
+ separate, `emit_event` simply had none, and every event descriptor shipped
524
+ without the `.by*` helpers its curated sibling had.
525
+ """
526
+ specs = {}
527
+ for field in fields:
528
+ name = field.get("name")
529
+ if not name:
530
+ continue
531
+ merged = dict(field)
532
+ if (not isinstance(merged.get("reference"), dict)
533
+ and catalog_references and name in catalog_references):
534
+ merged["reference"] = catalog_references[name]
535
+ if classify(merged, invariant) != "lookup":
536
+ continue
537
+ reference = reference_of(merged)
538
+ names = [str(n) for n in (reference.get("lookupNames") or [])]
539
+ spec = {
540
+ "path": reference.get("path") or "",
541
+ "by": names,
542
+ "value": str(reference.get("lookupValue")),
543
+ "aliases": by_aliases(names),
544
+ "strategy": lookup_strategy(reference),
545
+ }
546
+ if reference.get("objectName"):
547
+ spec["objectName"] = str(reference["objectName"])
548
+ if reference.get("dependsOn"):
549
+ spec["dependsOn"] = [str(d) for d in reference["dependsOn"]]
550
+ specs[name] = spec
551
+ return specs
552
+
553
+
554
+ def mark_lookup_fields(entry: dict, fields: list[dict], invariant: set) -> list[dict]:
555
+ """Flag the fields whose TS type must also accept a `LookupToken`.
556
+
557
+ Exactly the set `lookup_specs` generates helpers for, decided by the same
558
+ call, so the type and the helper cannot disagree about which fields are
559
+ resolvable.
560
+ """
561
+ catalog_references = {
562
+ f.get("name"): f["reference"]
563
+ for f in ((entry.get("inputSchema") or {}).get("fields") or [])
564
+ if isinstance(f.get("reference"), dict)
565
+ }
566
+ out = []
567
+ for field in fields:
568
+ merged = dict(field)
569
+ name = merged.get("name")
570
+ if not isinstance(merged.get("reference"), dict) and name in catalog_references:
571
+ merged["reference"] = catalog_references[name]
572
+ if classify(merged, invariant) == "lookup":
573
+ merged["__lookup"] = True
574
+ out.append(merged)
575
+ return out
576
+
577
+
578
+ def emit_lookup_comment(specs: dict, symbol: str = "<Descriptor>") -> list[str]:
579
+ """The grep-able header — route 3 of the design's discoverability section.
580
+
581
+ The agents this surface is for run with Bash/Read/Grep and no language
582
+ server, so a `.byEmail` discoverable only by hovering is invisible. The
583
+ descriptor module is real source and is already opened to check a name, so
584
+ the lookups are stated there in prose beside the data.
585
+ """
586
+ lines = []
587
+ for field, spec in specs.items():
588
+ target = str(spec.get("objectName") or spec["path"]).split("?", 1)[0]
589
+ lines.append(f" * LOOKUP: `{field}` -> {target}.{spec['value']}")
590
+ aliases = spec["aliases"]
591
+ offered = [f"{a}()" for a in aliases.values()] if aliases else []
592
+ by_list = ", ".join(f"`{n}`" for n in spec["by"])
593
+ lines.append(f" * by: {by_list}")
594
+ # THE WHOLE CALL, not just the method names. Naming `.byEmail` without
595
+ # the `lookup(Descriptor, 'field')` wrapper tells a reader that helpers
596
+ # exist but not how to reach one — measured: an agent read this comment,
597
+ # learned the helper names, and then made three separate reads of
598
+ # api.md to find the signature. One line here removes that trip.
599
+ first = next(iter(aliases.values()), None)
600
+ call = (f"lookup({symbol}, '{field}').{first}(value)" if first
601
+ else f"lookup({symbol}, '{field}').by('{spec['by'][0]}', value)"
602
+ if spec["by"] else f"lookup({symbol}, '{field}')")
603
+ lines.append(f" * use: {call}")
604
+ if offered:
605
+ lines.append(f" * also: {', '.join('.' + a for a in aliases.values())}"
606
+ f" · .by(name, value)")
607
+ else:
608
+ lines.append(" * also: .by(name, value) only — aliases collided")
609
+ lines.append(f" * strategy: {STRATEGY_PROSE[spec['strategy']]}")
610
+ if spec.get("dependsOn"):
611
+ lines.append(f" * resolve first: {', '.join('`' + d + '`' for d in spec['dependsOn'])}")
612
+ return lines
613
+
614
+
615
+ STRATEGY_PROSE = {
616
+ "filter": "one request (the operation declares a server-side filter)",
617
+ "scan": "paged scan (this operation declares no filterPattern)",
618
+ "tree": "hierarchical walk (the collection is a tree)",
619
+ "dependent": "dependent — another field must resolve first",
620
+ }
621
+
622
+
623
+ def emit_event(
624
+ entry: dict,
625
+ in_fields: list[dict],
626
+ out_fields: list[dict],
627
+ input_tree: dict[str, TypeNode],
628
+ output_tree: dict[str, TypeNode],
629
+ name: str,
630
+ input_types: NamedTypeRegistry,
631
+ output_types: NamedTypeRegistry,
632
+ invariant: set | None = None,
633
+ ) -> list[str]:
634
+ """Emit a connector EVENT as a `triggerDescriptor()`, the shape `onEvent()` takes.
635
+
636
+ An event entry (`uipath.connector.event.<key>.<event>`, or the curated
637
+ `.trigger.` sibling) is not an action: `onEvent`/`waitForEvent` identify it by
638
+ `connector` + `event`, and a `descriptor()` const — the action shape this
639
+ generator used to emit for it — fails their positive descriptor test, so the
640
+ subscription lost its connector and event (EVENT_NO_CONNECTOR/EVENT_NO_EVENT)
641
+ and every author hand-wrote a `triggerDescriptor()` instead. The payload type
642
+ comes from `outputSchema`, and `where` from the entry's event parameters —
643
+ a GENERIC event's object is the author's `object` option, not a field here.
644
+
645
+ `where` was `Record<string, string>` while an event entry carried no
646
+ parameters to type it from. It does now, so a misspelt parameter is a
647
+ compile error rather than a `check` warning. The fallback stays for an
648
+ entry that declares NONE: that is either an event which genuinely takes no
649
+ parameters (HTTP Webhook — the connection is the subscription's whole
650
+ scope) or one nothing has described yet, and an empty interface would
651
+ refuse every key in both cases — wrongly in the second.
652
+ """
653
+ node_type = entry["nodeType"]
654
+ event_id = node_type.rsplit(".", 1)[-1]
655
+ connector_key = (entry.get("connector") or {}).get("key") or ""
656
+ label = (entry.get("display") or {}).get("label") or event_id
657
+ version = entry.get("version", "1.0.0")
658
+ generic = (entry.get("operation") or {}).get("activityType") == "GenericTrigger"
659
+ lines: list[str] = []
660
+ where_type = "Record<string, string>"
661
+ if in_fields:
662
+ where_type = f"{name}Where"
663
+ lines += emit_interface(
664
+ where_type, in_fields, all_optional=False,
665
+ named_types=input_types, field_tree=input_tree, is_input=True,
666
+ )
667
+ lines.append("")
668
+ lines += emit_interface(
669
+ f"{name}Outputs", out_fields, all_optional=True,
670
+ named_types=output_types, field_tree=output_tree,
671
+ )
672
+ lines.append("")
673
+ # A subscription's `where` values are resolved by `lookup()` exactly as a
674
+ # connector input's are, and `prepare --resolve` reads the same reference
675
+ # block — so the descriptor has to carry the same specs. Emitting none is
676
+ # what made `lookup(EmailReceived, 'parentFolderId').byDisplayName('Inbox')`
677
+ # fail with a bare TypeError while the curated sibling's `.byDisplayName`
678
+ # worked, on a field both forms declare identically.
679
+ specs = specs_from_fields(in_fields, invariant or set())
680
+ lines.append("/**")
681
+ lines.append(f" * {label} — {node_type}")
682
+ lines.append(" *")
683
+ if generic:
684
+ lines.append(" * A GENERIC event: it fires for ONE object of the connection (an entity, a")
685
+ lines.append(" * table, a custom object), so name it — `onEvent(" + name + ", { object: '<Object>' })`.")
686
+ lines.append(f" * `uip is triggers objects {connector_key} <EVENT> --connection-id <id>` lists them.")
687
+ else:
688
+ lines.append(f" * Start a flow on it with `onEvent({name}, {{ where: {{ … }} }})`, or pause for it")
689
+ lines.append(f" * with `waitForEvent({name}, {{ … }})`; `where` carries its event parameters.")
690
+ # INSIDE the block, as `emit_op` does. `emit_lookup_comment` returns bare
691
+ # ` * ` body lines; prepending them to the module instead emitted a comment
692
+ # with no `/**` opener, and every connector module holding an event with a
693
+ # lookup stopped parsing — which no assertion on substrings could see.
694
+ if specs:
695
+ lines.append(" *")
696
+ lines += emit_lookup_comment(specs, name)
697
+ lines.append(" */")
698
+ lines.append(f"export const {name} = triggerDescriptor<{where_type}, {name}Outputs>()({{")
699
+ lines.append(f" connector: {json.dumps(connector_key)},")
700
+ lines.append(f" event: {json.dumps(event_id)},")
701
+ lines.append(f" version: {json.dumps(version)},")
702
+ lines.append(f" displayName: {json.dumps(label)},")
703
+ lines += emit_lookups_block(specs)
704
+ lines.append("} as const);")
705
+ return lines
706
+
707
+
708
+ def emit_op(
709
+ entry: dict,
710
+ v1def: dict,
711
+ input_tree: dict[str, TypeNode],
712
+ output_tree: dict[str, TypeNode],
713
+ name: str,
714
+ input_types: NamedTypeRegistry,
715
+ output_types: NamedTypeRegistry,
716
+ invariant: set | None = None,
717
+ ) -> list[str]:
718
+ node_type = entry["nodeType"]
719
+ action_id = node_type.rsplit(".", 1)[-1]
720
+ operation = entry.get("operation") or {}
721
+ is_generic_expanded = operation.get("activityType") == "GenericExpanded"
722
+ label = (entry.get("display") or {}).get("label") or action_id
723
+ version = entry.get("version", "1.0.0")
724
+ runtime = entry.get("runtime", {}) or {}
725
+ in_fields = descriptor_input_fields(entry, v1def)
726
+ out_fields = (entry.get("outputSchema") or {}).get("fields") or []
727
+
728
+ if is_event_node(node_type):
729
+ return emit_event(entry, in_fields, out_fields, input_tree, output_tree,
730
+ name, input_types, output_types, invariant)
731
+
732
+ lines: list[str] = []
733
+ lines += emit_interface(
734
+ f"{name}Inputs", in_fields, all_optional=False,
735
+ named_types=input_types, field_tree=input_tree, is_input=True,
736
+ )
737
+ lines.append("")
738
+ lines += emit_interface(
739
+ f"{name}Outputs",
740
+ out_fields,
741
+ all_optional=True,
742
+ named_types=output_types,
743
+ field_tree=output_tree,
744
+ )
745
+ lines.append("")
746
+ # An operation whose real field set arrives at design time has an `Inputs`
747
+ # type that is either empty (no object chosen yet) or just the parents.
748
+ # Nothing in the type says so, and the Markdown page that does say it is a
749
+ # different artifact — so a developer reading types alone concludes the
750
+ # operation takes exactly what is written here. The doc comment is the only
751
+ # place the typed surface can carry it.
752
+ schema_action = operation.get("schemaAction") or {}
753
+ specs = lookup_specs(entry, v1def, invariant or set())
754
+ lookup_comment = emit_lookup_comment(specs, name) if specs else []
755
+ if operation.get("objectFromDiscovery"):
756
+ lines.append("/**")
757
+ lines.append(f" * {label} — {node_type}")
758
+ lines.append(" *")
759
+ lines.append(" * The object is chosen against the connection, so `Inputs` is empty")
760
+ lines.append(" * here. List the objects and read the chosen one's fields with")
761
+ lines.append(" * `uip is resources list` / `describe` before binding this node.")
762
+ if lookup_comment:
763
+ lines.append(" *")
764
+ lines += lookup_comment
765
+ lines.append(" */")
766
+ elif schema_action.get("parents"):
767
+ parents = ", ".join(f"`{x}`" for x in schema_action["parents"])
768
+ lines.append("/**")
769
+ lines.append(f" * {label} — {node_type}")
770
+ lines.append(" *")
771
+ lines.append(f" * `Inputs` carries only the fields that SELECT the schema ({parents}).")
772
+ lines.append(f" * The real set comes from the `{schema_action.get('name')}` design-time")
773
+ lines.append(" * action — describe with those values supplied to see it.")
774
+ if lookup_comment:
775
+ lines.append(" *")
776
+ lines += lookup_comment
777
+ lines.append(" */")
778
+ elif lookup_comment:
779
+ lines.append("/**")
780
+ lines.append(f" * {label} — {node_type}")
781
+ lines.append(" *")
782
+ lines += lookup_comment
783
+ lines.append(" */")
784
+ else:
785
+ lines.append(f"/** {label} — {node_type} */")
786
+ lines.append(f"export const {name} = descriptor<{name}Inputs, {name}Outputs>()({{")
787
+ lines.append(f" nodeType: {json.dumps(node_type)},")
788
+ lines.append(f" version: {json.dumps(version)},")
789
+ if is_generic_expanded and operation.get("objectName"):
790
+ lines.append(f" objectName: {json.dumps(operation['objectName'])},")
791
+ if runtime.get("requiresConnection"):
792
+ lines.append(" requiresConnection: true,")
793
+ if runtime.get("requiresFolderKey"):
794
+ lines.append(" requiresFolderKey: true,")
795
+ lines += emit_lookups_block(specs)
796
+ lines.append("} as const);")
797
+ return lines
798
+
799
+
800
+ def emit_lookups_block(specs: dict) -> list[str]:
801
+ """The descriptor's `lookups:` literal — a RUNTIME value, not a type.
802
+
803
+ `lookup()` reads `aliases` from it to build the `.byName()` family, so a
804
+ descriptor emitted without this block offers only the generic
805
+ `.by(name, value)` and any alias call fails as a plain `TypeError`.
806
+ """
807
+ if not specs:
808
+ return []
809
+ lines = [" lookups: {"]
810
+ for field, spec in specs.items():
811
+ lines.append(f" {key_literal(field)}: {{")
812
+ if spec.get("objectName"):
813
+ lines.append(f" objectName: {json.dumps(spec['objectName'])},")
814
+ lines.append(f" path: {json.dumps(spec['path'])},")
815
+ lines.append(f" by: {json.dumps(spec['by'])},")
816
+ lines.append(f" value: {json.dumps(spec['value'])},")
817
+ aliases = ", ".join(
818
+ f"{alias}: {json.dumps(name)}" for name, alias in spec["aliases"].items()
819
+ )
820
+ lines.append(f" aliases: {{{(' ' + aliases + ' ') if aliases else ''}}},")
821
+ lines.append(f" strategy: {json.dumps(spec['strategy'])},")
822
+ if spec.get("dependsOn"):
823
+ lines.append(f" dependsOn: {json.dumps(spec['dependsOn'])},")
824
+ lines.append(" },")
825
+ lines.append(" },")
826
+ return lines
827
+
828
+
829
+ def main() -> None:
830
+ ap = argparse.ArgumentParser()
831
+ ap.add_argument("--library", required=True)
832
+ ap.add_argument("--output", required=True)
833
+ ap.add_argument("--import", dest="imp", default="@uipath/maestro-builder-sdk",
834
+ help="module specifier for `import { descriptor }`")
835
+ ap.add_argument("--connectors", default="", help="csv subset of connector keys (default: all)")
836
+ ap.add_argument(
837
+ "--include-generic-expanded", action="store_true",
838
+ help="emit typed descriptors for GenericExpanded entries (used by "
839
+ "small connection-resolved overlays; off for the bulk library)",
840
+ )
841
+ args = ap.parse_args()
842
+
843
+ lib = Path(args.library)
844
+ idx = json.loads((lib / "index.json").read_text())
845
+ # The invariant-collection ledger sits beside the index. Absent reads as
846
+ # empty, which is the permissive pre-ledger behaviour (see _lookups.py).
847
+ invariant = load_invariant_collections(lib)
848
+ subset = {c for c in args.connectors.split(",") if c} or None
849
+
850
+ # group Curated + http-request entries by connector key
851
+ by_conn: dict[str, list[dict]] = {}
852
+ for e in idx["entries"]:
853
+ if e.get("activityType") == "GenericExpanded" and not args.include_generic_expanded:
854
+ continue
855
+ # A generic EVENT's per-object variants share their template's nodeType
856
+ # and so would emit the same const twice — a module that does not
857
+ # compile. Only the template gets a descriptor, which is also the right
858
+ # surface: `onEvent(RecordCreated, { object: 'Message' })` names the
859
+ # object as an OPTION, so one descriptor serves every object. The
860
+ # variants still back `check` and `compile`, which resolve by object.
861
+ if (e.get("activityType") == "GenericTrigger" and e.get("objectName")
862
+ and not args.include_generic_expanded):
863
+ continue
864
+ ck = e["connectorKey"]
865
+ if subset and ck not in subset:
866
+ continue
867
+ by_conn.setdefault(ck, []).append(e)
868
+
869
+ out_dir = Path(args.output)
870
+ out_dir.mkdir(parents=True, exist_ok=True)
871
+ written = 0
872
+ required_input_gaps: list[str] = []
873
+ # ONE symbol map for the whole run, shared with convert_library_to_md.py so
874
+ # the markdown can cite these names without re-deriving them (see
875
+ # _naming.symbol_index for why re-deriving cannot be made safe).
876
+ names = symbol_index(idx["entries"], args.include_generic_expanded)
877
+
878
+ for ck, entries in sorted(by_conn.items()):
879
+ input_types = NamedTypeRegistry(
880
+ suffix="Input", all_optional=False, merge_reused=False, is_input=True,
881
+ )
882
+ output_types = NamedTypeRegistry()
883
+ loaded: list[tuple[dict, dict, dict[str, TypeNode], dict[str, TypeNode]]] = []
884
+ for e in sorted(entries, key=lambda x: x["nodeType"]):
885
+ entry = json.loads((lib / e["path"]).read_text())
886
+ v1def_raw = json.loads((lib / e["path"].replace(".json", ".v1def.json")).read_text())
887
+ v1def = v1def_raw[0] if isinstance(v1def_raw, list) else v1def_raw
888
+ is_event = is_event_node(e["nodeType"])
889
+ operation_input_fields = mark_lookup_fields(
890
+ entry, descriptor_input_fields(entry, v1def), invariant,
891
+ )
892
+ output_fields = (entry.get("outputSchema") or {}).get("fields") or []
893
+ input_tree = build_type_tree(operation_input_fields)
894
+ # The required-input gap is a statement about a connector ACTION's
895
+ # inputs; an event's parameters are optional by construction (an
896
+ # unscoped subscription is legal, `check` only warns).
897
+ gap = None if is_event else required_input_gap(entry, input_tree)
898
+ if gap:
899
+ required_input_gaps.append(gap)
900
+ output_tree = build_type_tree(output_fields)
901
+ input_types.discover(input_tree)
902
+ output_types.discover(output_tree)
903
+ loaded.append((entry, v1def, input_tree, output_tree))
904
+
905
+ operations: list[str] = []
906
+ for entry, v1def, input_tree, output_tree in loaded:
907
+ operations += emit_op(
908
+ entry, v1def, input_tree, output_tree, names[entry["nodeType"]],
909
+ input_types, output_types, invariant,
910
+ )
911
+ operations.append("")
912
+
913
+ emitted_types = input_types.emit()
914
+ rendered = "\n".join(emitted_types + operations)
915
+ specifiers = [s for s in ("descriptor", "triggerDescriptor") if f"= {s}<" in rendered] or ["descriptor"]
916
+ for alias in ("ConnectorLookupValue", "ConnectorValue"):
917
+ if alias in rendered:
918
+ specifiers.append(f"type {alias}")
919
+ body: list[str] = [
920
+ f"import {{ {', '.join(specifiers)} }} from {json.dumps(args.imp)};", "",
921
+ ]
922
+ body += emitted_types
923
+ body += output_types.emit()
924
+ body += operations
925
+ (out_dir / f"{ck}.ts").write_text("\n".join(body).rstrip() + "\n")
926
+ written += 1
927
+ if required_input_gaps:
928
+ sys.exit(
929
+ "connector wrapper generation stopped:\n- "
930
+ + "\n- ".join(required_input_gaps)
931
+ )
932
+ print(f"generated {written} connector module(s) → {out_dir}")
933
+
934
+
935
+ if __name__ == "__main__":
936
+ main()