@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,355 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Deterministic discovery for connector EVENTS — triggers and mid-flow waits.
|
|
3
|
+
|
|
4
|
+
The activities pipeline resolves a connector operation's schema through
|
|
5
|
+
`uip is resources list`/`describe`. Events have their own twin pair —
|
|
6
|
+
`uip is triggers objects` / `uip is triggers describe` — plus the product
|
|
7
|
+
registry's node manifest (`uip maestro flow registry get`, which for trigger
|
|
8
|
+
node types REFUSES to answer without a live `--connection-id`, and which
|
|
9
|
+
mirrors the describe sections when it does).
|
|
10
|
+
|
|
11
|
+
Two shapes matter here and they are deliberately kept apart:
|
|
12
|
+
|
|
13
|
+
- `triggers objects` answers PER EVENT OBJECT: the object catalog a generic
|
|
14
|
+
event ranges over, and per object the facts that change how a subscription
|
|
15
|
+
must be prepared (`ByoaConnection`, `IsWebhookUrlVisible`) and the
|
|
16
|
+
`Parameters` the object contributes to the input set.
|
|
17
|
+
- `triggers describe` answers PER (event, object): `EventParameters`,
|
|
18
|
+
`FilterFields`, `OutputFields`.
|
|
19
|
+
|
|
20
|
+
The V1 skill's source-of-truth contract is preserved verbatim: the complete
|
|
21
|
+
event-input set is the UNION of `objects → Parameters` and
|
|
22
|
+
`describe → EventParameters`, required if EITHER side says required. A field
|
|
23
|
+
can appear in one and not the other (measured in the V1 corpus: required
|
|
24
|
+
fields present only in describe), so neither call alone is trusted.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import subprocess
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"EventDiscoveryError",
|
|
34
|
+
"triggers_objects",
|
|
35
|
+
"triggers_describe",
|
|
36
|
+
"camel_fields",
|
|
37
|
+
"normalize_bucket_fields",
|
|
38
|
+
"union_event_parameters",
|
|
39
|
+
"merge_describe_into_node",
|
|
40
|
+
"event_facts",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# `Parameters[].type` on a `triggers objects` row routes the value into one of
|
|
44
|
+
# three `--detail` buckets at emit time. Everything unrecognized is an event
|
|
45
|
+
# parameter — that is the designer's own fallback, and inventing a fourth
|
|
46
|
+
# bucket here would emit a detail key the validator refuses.
|
|
47
|
+
_BUCKETS = {"query": "query", "path": "path"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EventDiscoveryError(Exception):
|
|
51
|
+
"""A trigger metadata call failed, with the reason and the way forward."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _run_json(command: list[str]) -> dict:
|
|
55
|
+
result = subprocess.run(command, capture_output=True, text=True)
|
|
56
|
+
try:
|
|
57
|
+
payload = json.loads(result.stdout)
|
|
58
|
+
except ValueError:
|
|
59
|
+
raise EventDiscoveryError(
|
|
60
|
+
f"`{' '.join(command[:5])} …` did not answer JSON: "
|
|
61
|
+
f"{(result.stderr or result.stdout).strip()[:300] or 'no output'}"
|
|
62
|
+
)
|
|
63
|
+
if payload.get("Result") != "Success":
|
|
64
|
+
raise EventDiscoveryError(
|
|
65
|
+
f"`{' '.join(command[:5])} …` failed: "
|
|
66
|
+
f"{payload.get('Message') or payload.get('Result')}"
|
|
67
|
+
)
|
|
68
|
+
return payload
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def triggers_objects(
|
|
72
|
+
key: str, operation: str, connection_id: str | None = None,
|
|
73
|
+
) -> list[dict]:
|
|
74
|
+
"""The event objects for `(connector, operation)`, PascalCase rows.
|
|
75
|
+
|
|
76
|
+
Rows carry `Name`/`DisplayName` (the same identity pair the activities
|
|
77
|
+
object ladder matches on) and, where the connector declares them,
|
|
78
|
+
`ByoaConnection`, `IsWebhookUrlVisible` and `Parameters`. Connectors that
|
|
79
|
+
declare none simply omit the keys (measured: sfdc's 1,178 CREATED rows
|
|
80
|
+
carry neither), so every reader treats them as optional.
|
|
81
|
+
|
|
82
|
+
`connection_id` is OPTIONAL, and what omitting it means depends on the
|
|
83
|
+
connector's object discovery. A connector whose objects do not come from
|
|
84
|
+
the connection answers identically either way (outlook `CREATED`:
|
|
85
|
+
Message + Calendar, with and without), which is what lets generation bake
|
|
86
|
+
those objects with no credentials at all. One whose objects DO come from
|
|
87
|
+
the connection answers with its declared set instead of the tenant's
|
|
88
|
+
(sfdc `CREATED`: 4 offline, 1,178 against a connection) — so a caller that
|
|
89
|
+
needs the tenant's objects must pass the id.
|
|
90
|
+
"""
|
|
91
|
+
command = ["uip", "is", "triggers", "objects", key, operation]
|
|
92
|
+
if connection_id:
|
|
93
|
+
command += ["--connection-id", connection_id]
|
|
94
|
+
payload = _run_json(command + ["--output", "json"])
|
|
95
|
+
data = payload.get("Data")
|
|
96
|
+
if isinstance(data, dict):
|
|
97
|
+
data = data.get("items")
|
|
98
|
+
return data if isinstance(data, list) else []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def triggers_describe(
|
|
102
|
+
key: str, operation: str, object_name: str,
|
|
103
|
+
connection_id: str | None = None,
|
|
104
|
+
) -> dict:
|
|
105
|
+
"""`EventParameters` / `FilterFields` / `OutputFields` for one object.
|
|
106
|
+
|
|
107
|
+
This call takes no node type — connector, operation and object only — so
|
|
108
|
+
it answers for the EVENT rather than for either of the two node forms the
|
|
109
|
+
platform ships it as. That is why both forms carry the same schema, and
|
|
110
|
+
why the product registry's per-node copy is a mirror of this: a lossy one,
|
|
111
|
+
measured on outlook `email-received` at 14 filter fields to this call's 15.
|
|
112
|
+
|
|
113
|
+
`connection_id` is optional here too; see `triggers_objects` for what
|
|
114
|
+
omitting it costs on a connector whose objects come from the connection.
|
|
115
|
+
"""
|
|
116
|
+
command = ["uip", "is", "triggers", "describe", key, operation, object_name]
|
|
117
|
+
if connection_id:
|
|
118
|
+
command += ["--connection-id", connection_id]
|
|
119
|
+
payload = _run_json(command + ["--output", "json"])
|
|
120
|
+
return payload.get("Data") or {}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _camelize(value):
|
|
124
|
+
"""Lower-case the first letter of every key, recursively.
|
|
125
|
+
|
|
126
|
+
`triggers describe` answers PascalCase all the way down, including the
|
|
127
|
+
`Reference` block: `{ObjectName, LookupNames, LookupValue, Path}`. Every
|
|
128
|
+
consumer of a reference — the lookup-spec generator, the `--resolve`
|
|
129
|
+
executor, the markdown — reads the registry's camelCase spelling, so a
|
|
130
|
+
verbatim copy is invisible to all of them: the field simply stops having
|
|
131
|
+
a lookup. One shape leaves this module, and this is what makes it one.
|
|
132
|
+
"""
|
|
133
|
+
if isinstance(value, dict):
|
|
134
|
+
return {
|
|
135
|
+
(k[0].lower() + k[1:] if k[:1].isupper() else k): _camelize(v)
|
|
136
|
+
for k, v in value.items()
|
|
137
|
+
}
|
|
138
|
+
if isinstance(value, list):
|
|
139
|
+
return [_camelize(v) for v in value]
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _is_hidden(low: dict) -> bool:
|
|
144
|
+
"""True for a hidden parameter that is safe to DROP — hidden and optional.
|
|
145
|
+
|
|
146
|
+
Outlook's `sharedMailboxAddress` is `Design.IsHidden: true` and optional:
|
|
147
|
+
a transport detail of the subscription, not something an author fills in,
|
|
148
|
+
so counting it would advertise a second field on an event whose only real
|
|
149
|
+
parameter is the mail folder.
|
|
150
|
+
|
|
151
|
+
A hidden parameter that is REQUIRED is a different animal and is kept
|
|
152
|
+
(flagged `hidden`, see `_keep_hidden_marker`). Asana's `parent` and
|
|
153
|
+
`projectId` are both — path-bucket, required, hidden — and dropping a
|
|
154
|
+
value the subscription needs at dispatch is exactly the silent wrongness
|
|
155
|
+
this design exists to remove. Keeping it flagged loses nothing: a surface
|
|
156
|
+
that should not prompt for it can read the flag, while nothing can lose
|
|
157
|
+
it by accident.
|
|
158
|
+
"""
|
|
159
|
+
design = low.get("design") or {}
|
|
160
|
+
if not isinstance(design, dict):
|
|
161
|
+
return False
|
|
162
|
+
# Only the TOP-level keys are lowered by the callers, so the nested block
|
|
163
|
+
# still reads `{"IsHidden": true}`. Camelize it before asking.
|
|
164
|
+
design = _camelize(design)
|
|
165
|
+
hidden = bool(design.get("isHidden") or design.get("hidden"))
|
|
166
|
+
return hidden and not low.get("required")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _keep_hidden_marker(low: dict, field: dict) -> dict:
|
|
170
|
+
"""Flag a kept parameter the designer hides (necessarily a required one)."""
|
|
171
|
+
design = _camelize(low.get("design") or {})
|
|
172
|
+
if isinstance(design, dict) and (design.get("isHidden") or design.get("hidden")):
|
|
173
|
+
field["hidden"] = True
|
|
174
|
+
return field
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _with_cascade(fields: list[dict]) -> list[dict]:
|
|
178
|
+
"""Mark each parameter that another one has to resolve before it.
|
|
179
|
+
|
|
180
|
+
Jira's `issuetype` reference is `/project/{project}/issuetypes` — the
|
|
181
|
+
cascade is written into the path, as a placeholder naming the parameter it
|
|
182
|
+
needs. That is the same encoding `_order_by_dependency` reads for an
|
|
183
|
+
activity's parent fields, and `_resolve.order_resolutions` already honours
|
|
184
|
+
`dependsOn`, so recording it here is what makes `prepare` resolve
|
|
185
|
+
`project` first without being told to.
|
|
186
|
+
"""
|
|
187
|
+
names = {f.get("name") for f in fields}
|
|
188
|
+
for field in fields:
|
|
189
|
+
reference = field.get("reference") or {}
|
|
190
|
+
path = str(reference.get("path") or "")
|
|
191
|
+
explicit = list(reference.get("dependsOn") or [])
|
|
192
|
+
depends = sorted(
|
|
193
|
+
other for other in names
|
|
194
|
+
if other and other != field.get("name")
|
|
195
|
+
and ("{" + str(other) + "}" in path or other in explicit)
|
|
196
|
+
)
|
|
197
|
+
if depends:
|
|
198
|
+
field["dependsOn"] = depends
|
|
199
|
+
reference.setdefault("dependsOn", depends)
|
|
200
|
+
# Designer order where the connector states one, so a cascade reads
|
|
201
|
+
# parent-first in every listing rather than in dict order.
|
|
202
|
+
fields.sort(key=lambda f: (f.get("sortOrder") is None, f.get("sortOrder") or 0))
|
|
203
|
+
return fields
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def camel_fields(fields: list | None) -> list[dict]:
|
|
207
|
+
"""Describe/objects fields normalized to the registry's camelCase shape.
|
|
208
|
+
|
|
209
|
+
`triggers describe` answers PascalCase (`Name`/`Type`/`DisplayName`);
|
|
210
|
+
the registry definition — and everything downstream that reads a v1def —
|
|
211
|
+
is camelCase. One shape leaves this module.
|
|
212
|
+
"""
|
|
213
|
+
out = []
|
|
214
|
+
for f in fields or []:
|
|
215
|
+
if not isinstance(f, dict):
|
|
216
|
+
continue
|
|
217
|
+
low = {k[0].lower() + k[1:] if k[:1].isupper() else k: v
|
|
218
|
+
for k, v in f.items()}
|
|
219
|
+
name = low.get("name")
|
|
220
|
+
if not name:
|
|
221
|
+
continue
|
|
222
|
+
if _is_hidden(low):
|
|
223
|
+
continue
|
|
224
|
+
field = {"name": name}
|
|
225
|
+
for k in ("displayName", "description", "required", "dataType",
|
|
226
|
+
"sortOrder"):
|
|
227
|
+
if low.get(k) is not None:
|
|
228
|
+
field[k] = low[k]
|
|
229
|
+
if low.get("reference") is not None:
|
|
230
|
+
field["reference"] = _camelize(low["reference"])
|
|
231
|
+
_keep_hidden_marker(low, field)
|
|
232
|
+
# Describe's `Type` is the DATA type; an objects-row Parameter's
|
|
233
|
+
# `type` is the BUCKET. The caller that knows which source it read
|
|
234
|
+
# passes bucketed fields through `normalize_bucket_fields` instead.
|
|
235
|
+
if low.get("type") is not None and "dataType" not in field:
|
|
236
|
+
field["type"] = low["type"]
|
|
237
|
+
field["dataType"] = low["type"]
|
|
238
|
+
elif "dataType" in field:
|
|
239
|
+
field["type"] = field["dataType"]
|
|
240
|
+
out.append(field)
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def normalize_bucket_fields(parameters: list | None) -> list[dict]:
|
|
245
|
+
"""`triggers objects → Parameters[]` rows, camelCase, bucket made explicit.
|
|
246
|
+
|
|
247
|
+
On these rows `type` selects the emit-time detail bucket (`query` →
|
|
248
|
+
`queryParameters`, `path` → `pathParameters`, anything else → event
|
|
249
|
+
parameters) and `dataType` is the value type — the OPPOSITE reading of
|
|
250
|
+
`type` from every other field list, which is exactly the kind of
|
|
251
|
+
quiet trap this normalization exists to bury.
|
|
252
|
+
"""
|
|
253
|
+
out = []
|
|
254
|
+
for f in parameters or []:
|
|
255
|
+
if not isinstance(f, dict):
|
|
256
|
+
continue
|
|
257
|
+
low = {k[0].lower() + k[1:] if k[:1].isupper() else k: v
|
|
258
|
+
for k, v in f.items()}
|
|
259
|
+
name = low.get("name")
|
|
260
|
+
if not name:
|
|
261
|
+
continue
|
|
262
|
+
if _is_hidden(low):
|
|
263
|
+
continue
|
|
264
|
+
field = {"name": name,
|
|
265
|
+
"bucket": _BUCKETS.get(str(low.get("type") or "").lower(), "event")}
|
|
266
|
+
for k in ("displayName", "description", "required", "sortOrder"):
|
|
267
|
+
if low.get(k) is not None:
|
|
268
|
+
field[k] = low[k]
|
|
269
|
+
if low.get("reference") is not None:
|
|
270
|
+
field["reference"] = _camelize(low["reference"])
|
|
271
|
+
data_type = low.get("dataType") or "string"
|
|
272
|
+
field["type"] = data_type
|
|
273
|
+
field["dataType"] = data_type
|
|
274
|
+
_keep_hidden_marker(low, field)
|
|
275
|
+
out.append(field)
|
|
276
|
+
return out
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def union_event_parameters(
|
|
280
|
+
declared: list | None, object_parameters: list | None,
|
|
281
|
+
) -> list[dict]:
|
|
282
|
+
"""The V1 union rule: describe's EventParameters ∪ the object row's Parameters.
|
|
283
|
+
|
|
284
|
+
Deduped by name; `required` is an OR — a field required on either side is
|
|
285
|
+
required, because each side is authoritative for fields the other omits.
|
|
286
|
+
The object row's bucket survives the merge; a field only describe knows
|
|
287
|
+
is an event parameter by definition.
|
|
288
|
+
"""
|
|
289
|
+
merged: dict[str, dict] = {}
|
|
290
|
+
for field in camel_fields(declared):
|
|
291
|
+
field.setdefault("bucket", "event")
|
|
292
|
+
merged[field["name"]] = field
|
|
293
|
+
for field in normalize_bucket_fields(object_parameters):
|
|
294
|
+
existing = merged.get(field["name"])
|
|
295
|
+
if existing is None:
|
|
296
|
+
merged[field["name"]] = field
|
|
297
|
+
continue
|
|
298
|
+
existing["required"] = bool(existing.get("required")) or bool(field.get("required"))
|
|
299
|
+
existing["bucket"] = field["bucket"]
|
|
300
|
+
for k in ("displayName", "description", "reference", "sortOrder"):
|
|
301
|
+
if field.get(k) is not None and existing.get(k) is None:
|
|
302
|
+
existing[k] = field[k]
|
|
303
|
+
return _with_cascade(list(merged.values()))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def merge_describe_into_node(
|
|
307
|
+
node: dict, describe: dict, object_parameters: list | None = None,
|
|
308
|
+
) -> dict:
|
|
309
|
+
"""Fold a `triggers describe` answer into a registry trigger definition.
|
|
310
|
+
|
|
311
|
+
The registry definition IS the v1def the overlay ships, and
|
|
312
|
+
`Library.eventParameterFields` already reads `v1def.eventParameters.fields`
|
|
313
|
+
— so the deterministic store is "the definition, with the describe
|
|
314
|
+
sections made true for this object" rather than a new format.
|
|
315
|
+
"""
|
|
316
|
+
node = dict(node)
|
|
317
|
+
node["eventParameters"] = {"fields": union_event_parameters(
|
|
318
|
+
describe.get("EventParameters"), object_parameters,
|
|
319
|
+
)}
|
|
320
|
+
node["filterFields"] = {"fields": camel_fields(describe.get("FilterFields"))}
|
|
321
|
+
if describe.get("OutputFields"):
|
|
322
|
+
node["outputResponseDefinition"] = {
|
|
323
|
+
"fields": camel_fields(describe.get("OutputFields"))}
|
|
324
|
+
for src, dst in (("EventMode", "eventMode"),
|
|
325
|
+
("ByoaConnection", "byoaConnection"),
|
|
326
|
+
("IsWebhookUrlVisible", "isWebhookUrlVisible")):
|
|
327
|
+
if describe.get(src) is not None:
|
|
328
|
+
node[dst] = describe[src]
|
|
329
|
+
return node
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def event_facts(node: dict, object_row: dict | None) -> dict:
|
|
333
|
+
"""The three facts that change how a subscription is prepared or tested.
|
|
334
|
+
|
|
335
|
+
Per-object answers (the `triggers objects` row, or a describe) win over
|
|
336
|
+
the definition's: `byoaConnection` and `isWebhookUrlVisible` are declared
|
|
337
|
+
PER EVENT OBJECT, not per connector — V1 measured connectors requiring
|
|
338
|
+
BYOA for some events and not others.
|
|
339
|
+
"""
|
|
340
|
+
row = object_row or {}
|
|
341
|
+
def _fact(row_key: str, node_key: str, default):
|
|
342
|
+
if row.get(row_key) is not None:
|
|
343
|
+
return row[row_key]
|
|
344
|
+
if node.get(node_key) is not None:
|
|
345
|
+
return node[node_key]
|
|
346
|
+
return default
|
|
347
|
+
return {
|
|
348
|
+
"eventMode": str(node.get("eventMode") or "polling"),
|
|
349
|
+
"byoaConnection": bool(_fact("ByoaConnection", "byoaConnection", False)),
|
|
350
|
+
"isWebhookUrlVisible": bool(_fact("IsWebhookUrlVisible", "isWebhookUrlVisible", False)),
|
|
351
|
+
"textBlocks": [
|
|
352
|
+
str(t) for t in ((row.get("Design") or {}).get("TextBlocks") or
|
|
353
|
+
(row.get("design") or {}).get("textBlocks") or [])
|
|
354
|
+
],
|
|
355
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Classify a connector input field's `reference` block, for both generators.
|
|
3
|
+
|
|
4
|
+
A `reference` with a `lookupValue` says a field's value is an opaque id that has
|
|
5
|
+
to be resolved against a live connection. 1,365 input fields across the shipped
|
|
6
|
+
library carry one. But not all of them are asking for a resolution, and
|
|
7
|
+
generating a `.by*` helper for the ones that are not is actively harmful: a
|
|
8
|
+
helper that shows up where it is not needed teaches a reader that the helper is
|
|
9
|
+
noise, and then it is ignored on the fields that need it.
|
|
10
|
+
|
|
11
|
+
Two classes are excluded, and they are excluded for different reasons.
|
|
12
|
+
|
|
13
|
+
IDENTITY LOOKUPS (231 fields). `lookupValue` is the only entry in `lookupNames`,
|
|
14
|
+
so the "resolution" is the identity function: call the API, match
|
|
15
|
+
`label == 'Qualified'`, send back `'Qualified'`. Nothing is translated — the
|
|
16
|
+
block enumerates legal values rather than mapping a name to an id. Detected
|
|
17
|
+
mechanically here.
|
|
18
|
+
|
|
19
|
+
INVARIANT COLLECTIONS (164 fields). The collection is a property of the
|
|
20
|
+
CONNECTOR's API rather than of the CONNECTION — a country list is identical in
|
|
21
|
+
every tenant — so the id cannot go stale on a move and resolving late buys
|
|
22
|
+
nothing. NOT detectable from the metadata: `system_countries -> name` and
|
|
23
|
+
`Users -> id` are structurally identical. Read from a curated ledger instead;
|
|
24
|
+
see `invariant-collections.json` for why that ledger is closed at three rows.
|
|
25
|
+
|
|
26
|
+
The fallback direction is deliberate and is the safe one. An unclassified field
|
|
27
|
+
is treated as an ordinary lookup, which is never *wrong* — only occasionally
|
|
28
|
+
ceremonial. The opposite default would drop the helper from a genuinely
|
|
29
|
+
connection-scoped field and hand the author back the silent-wrongness failure
|
|
30
|
+
the whole mechanism exists to remove.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"LEDGER_FILENAME",
|
|
40
|
+
"load_invariant_collections",
|
|
41
|
+
"reference_of",
|
|
42
|
+
"is_identity_lookup",
|
|
43
|
+
"classify",
|
|
44
|
+
"resolvable_lookup_fields",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
LEDGER_FILENAME = "invariant-collections.json"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_invariant_collections(library_dir: str | Path | None) -> set[str]:
|
|
51
|
+
"""The ledger's `objectName` set, or an empty set.
|
|
52
|
+
|
|
53
|
+
An absent or malformed ledger reads as EMPTY rather than raising, which
|
|
54
|
+
matches `loadListEnvelopes` in the SDK and is the same reasoning: the
|
|
55
|
+
permissive path is the pre-ledger behaviour, so the worst case of a bad edit
|
|
56
|
+
is losing the exactness, never breaking a build.
|
|
57
|
+
"""
|
|
58
|
+
if not library_dir:
|
|
59
|
+
return set()
|
|
60
|
+
path = Path(library_dir) / LEDGER_FILENAME
|
|
61
|
+
if not path.is_file():
|
|
62
|
+
return set()
|
|
63
|
+
try:
|
|
64
|
+
with path.open(encoding="utf-8") as fh:
|
|
65
|
+
parsed = json.load(fh)
|
|
66
|
+
except (OSError, ValueError):
|
|
67
|
+
return set()
|
|
68
|
+
rows = parsed.get("collections")
|
|
69
|
+
if not isinstance(rows, list):
|
|
70
|
+
return set()
|
|
71
|
+
return {
|
|
72
|
+
row["objectName"] for row in rows
|
|
73
|
+
if isinstance(row, dict) and isinstance(row.get("objectName"), str)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def reference_of(field: dict) -> dict | None:
|
|
78
|
+
"""The field's `reference` block when it declares a resolvable value."""
|
|
79
|
+
reference = field.get("reference")
|
|
80
|
+
if not isinstance(reference, dict):
|
|
81
|
+
return None
|
|
82
|
+
if not reference.get("lookupValue"):
|
|
83
|
+
return None
|
|
84
|
+
return reference
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_identity_lookup(reference: dict) -> bool:
|
|
88
|
+
"""True when resolving would return the value it was given."""
|
|
89
|
+
names = reference.get("lookupNames") or []
|
|
90
|
+
return len(names) == 1 and reference.get("lookupValue") in names
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def classify(field: dict, invariant: set[str]) -> str:
|
|
94
|
+
"""One of `'none'`, `'identity'`, `'invariant'`, `'lookup'`.
|
|
95
|
+
|
|
96
|
+
Only `'lookup'` earns a helper and a `--resolve`. The two degenerate classes
|
|
97
|
+
are reported distinctly rather than merged because the markdown treats them
|
|
98
|
+
differently: an identity lookup keeps its enumeration command (its legal
|
|
99
|
+
values exist nowhere offline), while an invariant one is just a value the
|
|
100
|
+
author already knows how to write.
|
|
101
|
+
"""
|
|
102
|
+
reference = reference_of(field)
|
|
103
|
+
if reference is None:
|
|
104
|
+
return "none"
|
|
105
|
+
if is_identity_lookup(reference):
|
|
106
|
+
return "identity"
|
|
107
|
+
if reference.get("objectName") in invariant:
|
|
108
|
+
return "invariant"
|
|
109
|
+
return "lookup"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolvable_lookup_fields(fields: list[dict], invariant: set[str]) -> list[dict]:
|
|
113
|
+
"""The subset of `fields` that earns a `.by*` helper, in declaration order."""
|
|
114
|
+
return [f for f in fields if classify(f, invariant) == "lookup"]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Symbol-name derivation shared by the two library generators.
|
|
3
|
+
|
|
4
|
+
`library-json/` feeds two derived views (see `lib/scripts/unpack-library.sh`):
|
|
5
|
+
`generate_connectors_ts.py` emits the TypeScript descriptors an author imports,
|
|
6
|
+
and `convert_library_to_md.py` emits the markdown an agent reads. The markdown
|
|
7
|
+
now cites the TypeScript **by name** — `connector(SendMessageToUser, …)`,
|
|
8
|
+
`.byEmail(…)` — and neither name exists anywhere in the JSON. Both are derived.
|
|
9
|
+
|
|
10
|
+
That makes the derivation a shared contract rather than a private detail, and
|
|
11
|
+
the failure mode of re-deriving it twice is the worst one available: a wrong
|
|
12
|
+
*description* costs a reader one re-read, but a wrong *symbol* is an import that
|
|
13
|
+
throws, printed by documentation, for a helper the reader was being told to
|
|
14
|
+
prefer over a route that already worked. They will not try the helper again.
|
|
15
|
+
|
|
16
|
+
So both generators import this module and neither owns a copy. The de-collision
|
|
17
|
+
loop especially cannot be duplicated even in principle — it is stateful across a
|
|
18
|
+
whole connector module, so a per-page re-derivation has no way to know that the
|
|
19
|
+
name it is about to print was already taken by an earlier operation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"pascal",
|
|
28
|
+
"singular",
|
|
29
|
+
"op_identity",
|
|
30
|
+
"op_symbol_name",
|
|
31
|
+
"symbol_index",
|
|
32
|
+
"by_alias",
|
|
33
|
+
"by_aliases",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def pascal(action_id: str) -> str:
|
|
38
|
+
"""`send-message-to-user` -> `SendMessageToUser`."""
|
|
39
|
+
return "".join(
|
|
40
|
+
w[:1].upper() + w[1:] for w in re.split(r"[^A-Za-z0-9]+", action_id) if w
|
|
41
|
+
) or "Op"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def singular(name: str) -> str:
|
|
45
|
+
"""Small deterministic singularizer for array field names used as type names."""
|
|
46
|
+
if name.lower().endswith("ies") and len(name) > 3:
|
|
47
|
+
return name[:-3] + "y"
|
|
48
|
+
if name.lower().endswith("sses"):
|
|
49
|
+
return name[:-2]
|
|
50
|
+
if name.lower().endswith("s") and not name.lower().endswith(("ss", "us")):
|
|
51
|
+
return name[:-1]
|
|
52
|
+
return name
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def op_identity(entry: dict) -> tuple[str, str, str]:
|
|
56
|
+
"""`(nodeType, activityType, label)` from either entry shape.
|
|
57
|
+
|
|
58
|
+
The same operation is described by two different JSON shapes: `index.json`
|
|
59
|
+
rows are flat (`activityType`, `label` at the top level) while the per-op
|
|
60
|
+
entry files nest them (`operation.activityType`, `display.label`). The
|
|
61
|
+
descriptor generator reads the nested one and the markdown generator walks
|
|
62
|
+
the flat one, so a naming helper that accepted only one of them would push
|
|
63
|
+
the caller into re-deriving the other — which is the drift this module
|
|
64
|
+
exists to prevent.
|
|
65
|
+
"""
|
|
66
|
+
node_type = entry.get("nodeType") or ""
|
|
67
|
+
operation = entry.get("operation")
|
|
68
|
+
if isinstance(operation, dict):
|
|
69
|
+
activity_type = operation.get("activityType") or ""
|
|
70
|
+
else:
|
|
71
|
+
activity_type = entry.get("activityType") or ""
|
|
72
|
+
display = entry.get("display")
|
|
73
|
+
if isinstance(display, dict) and display.get("label"):
|
|
74
|
+
label = display["label"]
|
|
75
|
+
else:
|
|
76
|
+
label = entry.get("label") or ""
|
|
77
|
+
if not label:
|
|
78
|
+
label = node_type.rsplit(".", 1)[-1]
|
|
79
|
+
return node_type, activity_type, label
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def op_symbol_name(entry: dict, seen: set[str]) -> str:
|
|
83
|
+
"""The exported const name for one operation, de-collided within its module.
|
|
84
|
+
|
|
85
|
+
`seen` is the per-connector-module set of names already emitted, and it is
|
|
86
|
+
MUTATED here — that is the whole point of centralising this. A module emits
|
|
87
|
+
its operations in index order, so two operations whose labels pascal to the
|
|
88
|
+
same symbol are distinguished by a trailing `_` on the later one, and which
|
|
89
|
+
one gets the underscore depends on the order the caller walks the entries in.
|
|
90
|
+
Both generators must therefore walk in the same order and share this set.
|
|
91
|
+
|
|
92
|
+
A `GenericExpanded` entry is named from its display label rather than its
|
|
93
|
+
action id, because its action id is the shared generic verb (`create`,
|
|
94
|
+
`list-all-records`) and would collide for every object the generic covers.
|
|
95
|
+
"""
|
|
96
|
+
node_type, activity_type, label = op_identity(entry)
|
|
97
|
+
action_id = node_type.rsplit(".", 1)[-1]
|
|
98
|
+
name = pascal(label if activity_type == "GenericExpanded" else action_id)
|
|
99
|
+
while name in seen:
|
|
100
|
+
name += "_"
|
|
101
|
+
seen.add(name)
|
|
102
|
+
return name
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def symbol_index(
|
|
106
|
+
entries: list[dict], include_generic_expanded: bool = False
|
|
107
|
+
) -> dict[str, str]:
|
|
108
|
+
"""`{nodeType: exportedConstName}` for every operation a module will export.
|
|
109
|
+
|
|
110
|
+
THE reason this exists. The descriptor generator groups entries by connector
|
|
111
|
+
and walks each group sorted by `nodeType`; the markdown generator walks
|
|
112
|
+
`index.json` in its own order, one operation at a time, and never groups. The
|
|
113
|
+
de-collision loop is stateful, so those two walks assign DIFFERENT names the
|
|
114
|
+
moment any connector has two operations whose labels collide — and the
|
|
115
|
+
markdown would then print an import that does not resolve.
|
|
116
|
+
|
|
117
|
+
Computing the map once, here, from the same grouping both must agree on, is
|
|
118
|
+
what makes that impossible rather than merely unlikely.
|
|
119
|
+
|
|
120
|
+
An operation with no entry in the returned map has no generated descriptor
|
|
121
|
+
(a `GenericExpanded` one, by default) and must be authored through the
|
|
122
|
+
library by key/action/object instead.
|
|
123
|
+
"""
|
|
124
|
+
by_connector: dict[str, list[dict]] = {}
|
|
125
|
+
for entry in entries:
|
|
126
|
+
node_type, activity_type, _ = op_identity(entry)
|
|
127
|
+
if activity_type == "GenericExpanded" and not include_generic_expanded:
|
|
128
|
+
continue
|
|
129
|
+
key = entry.get("connectorKey")
|
|
130
|
+
if not key:
|
|
131
|
+
# `uipath.connector.<key>.<action>` — the only other place the key lives.
|
|
132
|
+
rest = node_type.replace("uipath.connector.", "")
|
|
133
|
+
key = rest.rsplit(".", 1)[0] if "." in rest else rest
|
|
134
|
+
by_connector.setdefault(key, []).append(entry)
|
|
135
|
+
|
|
136
|
+
names: dict[str, str] = {}
|
|
137
|
+
for _key, group in sorted(by_connector.items()):
|
|
138
|
+
seen: set[str] = set()
|
|
139
|
+
for entry in sorted(group, key=lambda e: e.get("nodeType") or ""):
|
|
140
|
+
names[entry["nodeType"]] = op_symbol_name(entry, seen)
|
|
141
|
+
return names
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def by_alias(lookup_name: str) -> str:
|
|
145
|
+
"""`profile.email` -> `byEmail`; `profile.real_name` -> `byRealName`.
|
|
146
|
+
|
|
147
|
+
Only the LAST dotted segment is used. The leading path is where the value
|
|
148
|
+
lives in the vendor's record, which is a fact about their API rather than
|
|
149
|
+
about what the author is searching by — `byProfileEmail` would be noise, and
|
|
150
|
+
the generic `.by('profile.email', v)` is always available for the exact form.
|
|
151
|
+
"""
|
|
152
|
+
last = lookup_name.rsplit(".", 1)[-1]
|
|
153
|
+
words = [w for w in re.split(r"[^A-Za-z0-9]+", last) if w]
|
|
154
|
+
if not words:
|
|
155
|
+
return ""
|
|
156
|
+
head, *rest = words
|
|
157
|
+
return "by" + head[:1].upper() + head[1:] + "".join(
|
|
158
|
+
w[:1].upper() + w[1:] for w in rest
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def by_aliases(lookup_names: list[str]) -> dict[str, str]:
|
|
163
|
+
"""`{lookupName: alias}` for every name that earns an unambiguous alias.
|
|
164
|
+
|
|
165
|
+
Two `lookupNames` can camelise to one alias — `email` and `contact.email`
|
|
166
|
+
both give `byEmail` — and there is no non-arbitrary way to award it to one of
|
|
167
|
+
them. Rather than guess, BOTH are dropped from the alias map and reachable
|
|
168
|
+
only through the generic `.by(name, value)`. Silently binding `byEmail` to
|
|
169
|
+
whichever came first in the array would compile, run, and search the wrong
|
|
170
|
+
field, which is precisely the class of quiet wrongness the lookup surface
|
|
171
|
+
exists to remove.
|
|
172
|
+
|
|
173
|
+
An empty return is normal and means "generic form only".
|
|
174
|
+
"""
|
|
175
|
+
counts: dict[str, int] = {}
|
|
176
|
+
for name in lookup_names:
|
|
177
|
+
alias = by_alias(name)
|
|
178
|
+
if alias:
|
|
179
|
+
counts[alias] = counts.get(alias, 0) + 1
|
|
180
|
+
out: dict[str, str] = {}
|
|
181
|
+
for name in lookup_names:
|
|
182
|
+
alias = by_alias(name)
|
|
183
|
+
if alias and counts[alias] == 1:
|
|
184
|
+
out[name] = alias
|
|
185
|
+
return out
|