@uipath/maestro-builder-sdk 6.4.0 → 6.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bpmn/uipath-moddle.v1.json +21 -0
- package/dist/cli/commands/registry.js +68 -12
- package/dist/registry/archive/entry-policy.d.ts +36 -0
- package/dist/registry/archive/entry-policy.js +60 -0
- package/dist/registry/archive/extract.d.ts +31 -0
- package/dist/registry/archive/extract.js +92 -0
- package/dist/registry/archive/zip-reader.d.ts +38 -0
- package/dist/registry/archive/zip-reader.js +256 -0
- package/dist/registry/codecs/pyjson.d.ts +62 -0
- package/dist/registry/codecs/pyjson.js +208 -0
- package/dist/registry/compat/compare.d.ts +18 -0
- package/dist/registry/compat/compare.js +32 -0
- package/dist/registry/compat/paths.d.ts +4 -0
- package/dist/registry/compat/paths.js +17 -0
- package/dist/registry/compat/pystr.d.ts +50 -0
- package/dist/registry/compat/pystr.js +221 -0
- package/dist/registry/concurrency.d.ts +10 -0
- package/dist/registry/concurrency.js +34 -0
- package/dist/registry/curated-overlay.d.ts +36 -4
- package/dist/registry/curated-overlay.js +87 -14
- package/dist/registry/errors.d.ts +26 -0
- package/dist/registry/errors.js +38 -0
- package/dist/registry/extractor.d.ts +9 -10
- package/dist/registry/extractor.js +5 -15
- package/dist/registry/files.d.ts +26 -0
- package/dist/registry/files.js +160 -0
- package/dist/registry/generators/descriptors.d.ts +81 -0
- package/dist/registry/generators/descriptors.js +405 -0
- package/dist/registry/generators/fields.d.ts +46 -0
- package/dist/registry/generators/fields.js +111 -0
- package/dist/registry/generators/markdown/common.d.ts +21 -0
- package/dist/registry/generators/markdown/common.js +55 -0
- package/dist/registry/generators/markdown/index.d.ts +33 -0
- package/dist/registry/generators/markdown/index.js +37 -0
- package/dist/registry/generators/markdown/page.d.ts +8 -0
- package/dist/registry/generators/markdown/page.js +52 -0
- package/dist/registry/generators/markdown/pyvalue.d.ts +34 -0
- package/dist/registry/generators/markdown/pyvalue.js +106 -0
- package/dist/registry/generators/markdown/sections/choosing-object.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/choosing-object.js +52 -0
- package/dist/registry/generators/markdown/sections/example.d.ts +22 -0
- package/dist/registry/generators/markdown/sections/example.js +295 -0
- package/dist/registry/generators/markdown/sections/filtering.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/filtering.js +76 -0
- package/dist/registry/generators/markdown/sections/header.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/header.js +37 -0
- package/dist/registry/generators/markdown/sections/inputs.d.ts +30 -0
- package/dist/registry/generators/markdown/sections/inputs.js +151 -0
- package/dist/registry/generators/markdown/sections/lookups.d.ts +12 -0
- package/dist/registry/generators/markdown/sections/lookups.js +150 -0
- package/dist/registry/generators/markdown/sections/outputs.d.ts +3 -0
- package/dist/registry/generators/markdown/sections/outputs.js +9 -0
- package/dist/registry/generators/markdown/sections/resolving-fields.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/resolving-fields.js +139 -0
- package/dist/registry/generators/markdown/sections/schema-dynamic.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/schema-dynamic.js +86 -0
- package/dist/registry/generators/materialize.d.ts +30 -0
- package/dist/registry/generators/materialize.js +132 -0
- package/dist/registry/generators/md-symbols.d.ts +22 -0
- package/dist/registry/generators/md-symbols.js +58 -0
- package/dist/registry/generators/triggers.d.ts +15 -0
- package/dist/registry/generators/triggers.js +85 -0
- package/dist/registry/generators/type-registry.d.ts +50 -0
- package/dist/registry/generators/type-registry.js +154 -0
- package/dist/registry/generators/type-tree.d.ts +65 -0
- package/dist/registry/generators/type-tree.js +179 -0
- package/dist/registry/library-build/build.d.ts +57 -0
- package/dist/registry/library-build/build.js +286 -0
- package/dist/registry/library-build/canonical.d.ts +119 -0
- package/dist/registry/library-build/canonical.js +512 -0
- package/dist/registry/library-build/cli.d.ts +45 -0
- package/dist/registry/library-build/cli.js +182 -0
- package/dist/registry/library-build/context.d.ts +107 -0
- package/dist/registry/library-build/context.js +267 -0
- package/dist/registry/library-build/enrich.d.ts +36 -0
- package/dist/registry/library-build/enrich.js +158 -0
- package/dist/registry/library-build/events.d.ts +108 -0
- package/dist/registry/library-build/events.js +476 -0
- package/dist/registry/library-build/generic-expand.d.ts +77 -0
- package/dist/registry/library-build/generic-expand.js +363 -0
- package/dist/registry/library-build/plan.d.ts +71 -0
- package/dist/registry/library-build/plan.js +270 -0
- package/dist/registry/library-build/py.d.ts +45 -0
- package/dist/registry/library-build/py.js +167 -0
- package/dist/registry/library-build/schema-action.d.ts +38 -0
- package/dist/registry/library-build/schema-action.js +107 -0
- package/dist/registry/library-build/write.d.ts +6 -0
- package/dist/registry/library-build/write.js +26 -0
- package/dist/registry/library-reader.d.ts +20 -0
- package/dist/registry/library-reader.js +50 -0
- package/dist/registry/lookup-specs.d.ts +55 -0
- package/dist/registry/lookup-specs.js +140 -0
- package/dist/registry/materialize-cli.js +28 -14
- package/dist/registry/materializer.d.ts +25 -3
- package/dist/registry/materializer.js +45 -29
- package/dist/registry/model/library.d.ts +254 -0
- package/dist/registry/model/library.js +23 -0
- package/dist/registry/naming.d.ts +54 -0
- package/dist/registry/naming.js +142 -0
- package/dist/registry/orphans.d.ts +4 -0
- package/dist/registry/orphans.js +86 -0
- package/dist/registry/prepare/actions.d.ts +26 -0
- package/dist/registry/prepare/actions.js +94 -0
- package/dist/registry/prepare/connections.d.ts +181 -0
- package/dist/registry/prepare/connections.js +399 -0
- package/dist/registry/prepare/event.d.ts +96 -0
- package/dist/registry/prepare/event.js +268 -0
- package/dist/registry/prepare/handoff.d.ts +21 -0
- package/dist/registry/prepare/handoff.js +91 -0
- package/dist/registry/prepare/ledger.d.ts +104 -0
- package/dist/registry/prepare/ledger.js +216 -0
- package/dist/registry/prepare/messages.d.ts +190 -0
- package/dist/registry/prepare/messages.js +472 -0
- package/dist/registry/prepare/objects.d.ts +130 -0
- package/dist/registry/prepare/objects.js +318 -0
- package/dist/registry/prepare/overlay.d.ts +149 -0
- package/dist/registry/prepare/overlay.js +487 -0
- package/dist/registry/prepare/parents.d.ts +41 -0
- package/dist/registry/prepare/parents.js +122 -0
- package/dist/registry/prepare/pipeline.d.ts +91 -0
- package/dist/registry/prepare/pipeline.js +376 -0
- package/dist/registry/prepare/regenerate.d.ts +9 -0
- package/dist/registry/prepare/regenerate.js +50 -0
- package/dist/registry/prepare/resolve.d.ts +199 -0
- package/dist/registry/prepare/resolve.js +470 -0
- package/dist/registry/prepare/retry.d.ts +83 -0
- package/dist/registry/prepare/retry.js +109 -0
- package/dist/registry/prepare/source-import.d.ts +54 -0
- package/dist/registry/prepare/source-import.js +192 -0
- package/dist/registry/prepare/support.d.ts +41 -0
- package/dist/registry/prepare/support.js +178 -0
- package/dist/registry/prepare-cli.d.ts +16 -5
- package/dist/registry/prepare-cli.js +60 -53
- package/dist/registry/pull-cli.js +8 -7
- package/dist/registry/reporter.d.ts +26 -0
- package/dist/registry/reporter.js +41 -0
- package/dist/registry/uip/client.d.ts +99 -0
- package/dist/registry/uip/client.js +226 -0
- package/dist/registry/uip/commands.d.ts +40 -0
- package/dist/registry/uip/commands.js +80 -0
- package/dist/registry/uip/envelope.d.ts +37 -0
- package/dist/registry/uip/envelope.js +84 -0
- package/dist/registry/uip/response-cache.d.ts +28 -0
- package/dist/registry/uip/response-cache.js +57 -0
- package/lib/README.md +10 -8
- package/package.json +3 -3
- package/dist/generators/_connections.py +0 -331
- package/dist/generators/_events.py +0 -355
- package/dist/generators/_lookups.py +0 -114
- package/dist/generators/_naming.py +0 -185
- package/dist/generators/_objects.py +0 -299
- package/dist/generators/_resolve.py +0 -319
- package/dist/generators/convert_library_to_md.py +0 -1441
- package/dist/generators/extract_library.py +0 -125
- package/dist/generators/generate_connectors_ts.py +0 -936
- package/dist/generators/generate_library_json.py +0 -2527
- package/dist/generators/generate_triggers_ts.py +0 -110
- package/dist/generators/materialize_registry.py +0 -155
- package/dist/generators/prepare_connector.py +0 -1860
- package/dist/registry/python.d.ts +0 -26
- package/dist/registry/python.js +0 -46
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Find the connection for a connector, and write its bindings.
|
|
3
|
-
|
|
4
|
-
`prepare` already knows which connector it is preparing. Making the author go
|
|
5
|
-
and find that connector's connection id by hand — and then its folder key, and
|
|
6
|
-
then hand-write `bindings.json` around both — is work the command can do itself
|
|
7
|
-
from information it already has.
|
|
8
|
-
|
|
9
|
-
Measured on the eval corpus, that hand-work was 4 to 7 tool calls per task:
|
|
10
|
-
listing connections, filtering them, then (because nothing said the folder key
|
|
11
|
-
was on the connection record) three or four more spent searching Orchestrator
|
|
12
|
-
folders for one matching the symbolic name in the prompt.
|
|
13
|
-
|
|
14
|
-
THE ONE THING THIS MUST NOT DO IS GUESS. A tenant can carry several connections
|
|
15
|
-
for the same connector — this repo's test tenant has five for Slack — and they
|
|
16
|
-
are not interchangeable: a user who exists in one workspace does not exist in
|
|
17
|
-
another, so picking the wrong one makes a lookup unresolvable for a reason that
|
|
18
|
-
points nowhere near the actual mistake. That failure was observed directly
|
|
19
|
-
(`skill-flow-slack-dm-lookup` named the wrong workspace and burned its whole
|
|
20
|
-
turn budget), which is why ambiguity here is an error listing the candidates and
|
|
21
|
-
never a silent choice.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import json
|
|
27
|
-
import subprocess
|
|
28
|
-
|
|
29
|
-
__all__ = [
|
|
30
|
-
"ConnectionError_",
|
|
31
|
-
"byoa_connections",
|
|
32
|
-
"byoa_listing",
|
|
33
|
-
"candidate_connections",
|
|
34
|
-
"candidate_listing",
|
|
35
|
-
"discover_connection",
|
|
36
|
-
"lookup_connection",
|
|
37
|
-
"merge_bindings",
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class ConnectionError_(Exception):
|
|
42
|
-
"""No usable connection, with the reason and the way forward."""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _list(all_folders: bool, byoa: bool = False) -> list[dict]:
|
|
46
|
-
command = ["uip", "is", "connections", "list", "--output", "json"]
|
|
47
|
-
if all_folders:
|
|
48
|
-
command.insert(4, "--all-folders")
|
|
49
|
-
if byoa:
|
|
50
|
-
command.insert(4, "--byoa")
|
|
51
|
-
result = subprocess.run(command, capture_output=True, text=True)
|
|
52
|
-
if result.returncode != 0:
|
|
53
|
-
raise ConnectionError_(
|
|
54
|
-
"listing connections failed: "
|
|
55
|
-
f"{result.stderr.strip() or 'no output'}"
|
|
56
|
-
)
|
|
57
|
-
try:
|
|
58
|
-
payload = json.loads(result.stdout)
|
|
59
|
-
except ValueError as error:
|
|
60
|
-
raise ConnectionError_(f"connection listing was not JSON: {error}") from error
|
|
61
|
-
data = payload.get("Data")
|
|
62
|
-
if isinstance(data, dict):
|
|
63
|
-
data = data.get("items")
|
|
64
|
-
return data if isinstance(data, list) else []
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _matching(rows: list[dict], connector_key: str, name: str | None) -> list[dict]:
|
|
68
|
-
out = [
|
|
69
|
-
row for row in rows
|
|
70
|
-
if row.get("ConnectorKey") == connector_key
|
|
71
|
-
# A Failed connection is listed like any other and cannot serve a
|
|
72
|
-
# request; selecting one produces an error about the operation rather
|
|
73
|
-
# than about the connection, which sends the reader the wrong way.
|
|
74
|
-
and str(row.get("State", "")).lower() == "enabled"
|
|
75
|
-
]
|
|
76
|
-
if name:
|
|
77
|
-
out = [row for row in out if row.get("Name") == name]
|
|
78
|
-
return out
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def discover_connection(connector_key: str, name: str | None = None) -> dict:
|
|
82
|
-
"""The one enabled connection for `connector_key`, or raise.
|
|
83
|
-
|
|
84
|
-
Tries the DEFAULT listing first and only widens to `--all-folders` if that
|
|
85
|
-
finds nothing. That order matters and is not just politeness to the API: the
|
|
86
|
-
default listing is the caller's own folder, which on a real tenant is one
|
|
87
|
-
connection where the tenant-wide view is many. Widening first would turn an
|
|
88
|
-
unambiguous case into an ambiguous one.
|
|
89
|
-
"""
|
|
90
|
-
rows = _matching(_list(all_folders=False), connector_key, name)
|
|
91
|
-
scope = "your folder"
|
|
92
|
-
if not rows:
|
|
93
|
-
rows = _matching(_list(all_folders=True), connector_key, name)
|
|
94
|
-
scope = "the tenant"
|
|
95
|
-
|
|
96
|
-
if not rows:
|
|
97
|
-
qualifier = f" named {name!r}" if name else ""
|
|
98
|
-
raise ConnectionError_(
|
|
99
|
-
f"no enabled {connector_key} connection{qualifier} found in {scope}. "
|
|
100
|
-
f"List what exists with:\n"
|
|
101
|
-
f" uip is connections list --all-folders"
|
|
102
|
-
)
|
|
103
|
-
if len(rows) > 1:
|
|
104
|
-
# Every candidate line carries its id: a NAME is not a key on a tenant.
|
|
105
|
-
# The eval tenant holds three Slack connections all named
|
|
106
|
-
# `is-sandboxes` (two Enabled, in different folders), so the old remedy —
|
|
107
|
-
# "name the one you want with --connection <name>" — handed back the very
|
|
108
|
-
# flag the caller had just used, and the doc-following author dead-ended
|
|
109
|
-
# (skill-flow-e2e-escalation-orchestrator-paths, 2026-09-01: placeholder
|
|
110
|
-
# GUIDs hand-written into bindings.json, 401 at the Slack node).
|
|
111
|
-
listed = "\n".join(
|
|
112
|
-
f" {row.get('Name')} --connection-id {row.get('Id')}"
|
|
113
|
-
f" (folder: {row.get('Folder')})"
|
|
114
|
-
for row in rows
|
|
115
|
-
)
|
|
116
|
-
names = {str(row.get("Name")) for row in rows}
|
|
117
|
-
if name is not None or len(names) < len(rows):
|
|
118
|
-
remedy = (
|
|
119
|
-
"They share a name, so --connection <name> cannot pick one; pass "
|
|
120
|
-
"the id of the one in the folder you mean with --connection-id:"
|
|
121
|
-
)
|
|
122
|
-
else:
|
|
123
|
-
remedy = (
|
|
124
|
-
"Name the one you want with --connection <name>, or pass its id "
|
|
125
|
-
"with --connection-id:"
|
|
126
|
-
)
|
|
127
|
-
raise ConnectionError_(
|
|
128
|
-
f"{len(rows)} enabled {connector_key} connections in {scope}, and they "
|
|
129
|
-
f"are not interchangeable — a record that exists in one workspace need "
|
|
130
|
-
f"not exist in another. {remedy}\n{listed}"
|
|
131
|
-
)
|
|
132
|
-
return rows[0]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def candidate_connections(connector_key: str, name: str | None = None) -> list[dict]:
|
|
136
|
-
"""Every enabled connection for `connector_key`, own folder FIRST.
|
|
137
|
-
|
|
138
|
-
`discover_connection` answers "which one" and stops at the first non-empty
|
|
139
|
-
scope, which is right for picking a connection and wrong for retrying one:
|
|
140
|
-
when the own-folder listing holds exactly one, the tenant-wide connections
|
|
141
|
-
are never even looked at. So a lookup that exhausts the personal-workspace
|
|
142
|
-
connection reports a bad VALUE while the record sits in another folder's
|
|
143
|
-
connection (flow-builder-sdk#744, measured on two Jira connections where
|
|
144
|
-
only the second carried project `TS`).
|
|
145
|
-
|
|
146
|
-
Ordering is the same preference `discover_connection` encodes — the caller's
|
|
147
|
-
own folder is the likeliest and is tried first — but nothing is dropped.
|
|
148
|
-
Deduplicated by `Id`, because the tenant-wide listing repeats the own-folder
|
|
149
|
-
rows.
|
|
150
|
-
|
|
151
|
-
This does NOT decide anything, and in particular does not raise on several
|
|
152
|
-
matches: choosing among them is the caller's job, and the only honest way to
|
|
153
|
-
choose is evidence (a lookup that resolves on exactly one). That keeps this
|
|
154
|
-
module's rule intact — ambiguity is never resolved by guessing.
|
|
155
|
-
"""
|
|
156
|
-
ordered: list[dict] = []
|
|
157
|
-
seen: set[str] = set()
|
|
158
|
-
for all_folders in (False, True):
|
|
159
|
-
for row in _matching(_list(all_folders=all_folders), connector_key, name):
|
|
160
|
-
identifier = str(row.get("Id", ""))
|
|
161
|
-
if identifier and identifier in seen:
|
|
162
|
-
continue
|
|
163
|
-
seen.add(identifier)
|
|
164
|
-
ordered.append(row)
|
|
165
|
-
return ordered
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def candidate_listing(rows: list[dict], indent: str = " ") -> str:
|
|
169
|
-
"""The candidates, one pasteable `--connection-id` line each."""
|
|
170
|
-
return "\n".join(
|
|
171
|
-
f"{indent}{row.get('Name')} --connection-id {row.get('Id')}"
|
|
172
|
-
f" (folder: {row.get('Folder')})"
|
|
173
|
-
for row in rows
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _enabled_listing(rows: list[dict], connector_key: str) -> str:
|
|
178
|
-
"""The enabled connections for `connector_key`, one per line, pasteable."""
|
|
179
|
-
enabled = _matching(rows, connector_key, None)
|
|
180
|
-
if not enabled:
|
|
181
|
-
return (
|
|
182
|
-
f" (none — this tenant has no Enabled {connector_key} connection; "
|
|
183
|
-
"repair or create one in Integration Service first)"
|
|
184
|
-
)
|
|
185
|
-
return "\n".join(
|
|
186
|
-
f" {row.get('Name')} --connection-id {row.get('Id')}"
|
|
187
|
-
f" (folder: {row.get('Folder')})"
|
|
188
|
-
for row in enabled
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def lookup_connection(connector_key: str, connection_id: str) -> dict:
|
|
193
|
-
"""The record for an explicitly named connection, or raise unless it can serve.
|
|
194
|
-
|
|
195
|
-
An explicit `--connection-id` used to skip every check discovery applies, and
|
|
196
|
-
that skip is where a whole class of silent failures came from. A connection
|
|
197
|
-
in `State: Failed` is listed like any other, and `uip is resources describe`
|
|
198
|
-
against it answers `Result: Success` with NO request fields — the tenant has
|
|
199
|
-
no schema to give for a connection it cannot reach. `prepare` then wrote an
|
|
200
|
-
overlay with zero body inputs, printed "0 input field(s)" as a success, and
|
|
201
|
-
the next `compile` rejected every real field as unknown, sending the author
|
|
202
|
-
hunting through the SDK for a flag that does not exist.
|
|
203
|
-
|
|
204
|
-
Measured on the eval tenant (Salesforce `Opportunity` / `Update`): the Failed
|
|
205
|
-
connection returned 0 request fields, the Enabled one 13 (`StageName` among
|
|
206
|
-
them). The state was visible in the same `connections list` the author had
|
|
207
|
-
already run; a projection that dropped the `State` column hid it.
|
|
208
|
-
|
|
209
|
-
So: resolve the id against the tenant-wide listing, and refuse — naming the
|
|
210
|
-
state found and the enabled alternatives — rather than describe through it.
|
|
211
|
-
"""
|
|
212
|
-
rows = _list(all_folders=True)
|
|
213
|
-
match = next(
|
|
214
|
-
(row for row in rows if str(row.get("Id", "")) == str(connection_id)), None
|
|
215
|
-
)
|
|
216
|
-
alternatives = _enabled_listing(rows, connector_key)
|
|
217
|
-
if match is None:
|
|
218
|
-
raise ConnectionError_(
|
|
219
|
-
f"connection {connection_id} is not in this tenant's listing "
|
|
220
|
-
f"(`uip is connections list --all-folders`). Enabled {connector_key} "
|
|
221
|
-
f"connections:\n{alternatives}\n"
|
|
222
|
-
"Or omit --connection-id and prepare selects the connection itself."
|
|
223
|
-
)
|
|
224
|
-
actual_key = match.get("ConnectorKey")
|
|
225
|
-
if actual_key and actual_key != connector_key:
|
|
226
|
-
raise ConnectionError_(
|
|
227
|
-
f"connection {match.get('Name')} ({connection_id}) belongs to connector "
|
|
228
|
-
f"{actual_key}, not {connector_key}. Enabled {connector_key} "
|
|
229
|
-
f"connections:\n{alternatives}\n"
|
|
230
|
-
"Or omit --connection-id and prepare selects the connection itself."
|
|
231
|
-
)
|
|
232
|
-
state = str(match.get("State", "") or "")
|
|
233
|
-
if state.lower() != "enabled":
|
|
234
|
-
shown = state or "in an unknown state"
|
|
235
|
-
raise ConnectionError_(
|
|
236
|
-
f"connection {match.get('Name')} ({connection_id}) is {shown}, not "
|
|
237
|
-
"Enabled. A connection that is not Enabled answers field discovery "
|
|
238
|
-
"with an EMPTY schema, so the overlay would carry no fields and the "
|
|
239
|
-
"next compile would still reject every input as unknown. Enabled "
|
|
240
|
-
f"{connector_key} connections:\n{alternatives}\n"
|
|
241
|
-
"Or omit --connection-id and prepare selects the connection itself."
|
|
242
|
-
)
|
|
243
|
-
return match
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def byoa_connections(connector_key: str) -> list[dict]:
|
|
247
|
-
"""The enabled BYOA connections for `connector_key`.
|
|
248
|
-
|
|
249
|
-
RAISES on a failed listing rather than answering an empty list. The
|
|
250
|
-
difference is the whole point: "you have no BYOA connection" tells the
|
|
251
|
-
caller to go create one, and a listing that failed says nothing of the
|
|
252
|
-
kind. Answering [] for both turns a broken call into confident, wrong
|
|
253
|
-
advice — the failure-is-not-emptiness rule this module already applies to
|
|
254
|
-
ordinary discovery.
|
|
255
|
-
"""
|
|
256
|
-
return _matching(_list(all_folders=True, byoa=True), connector_key, None)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def byoa_listing(rows: list[dict], connector_key: str) -> str:
|
|
260
|
-
"""The BYOA candidates, one pasteable line each, or why there are none."""
|
|
261
|
-
if not rows:
|
|
262
|
-
return (
|
|
263
|
-
f" (none — this tenant has no enabled BYOA {connector_key} "
|
|
264
|
-
"connection; create one in Integration Service with your own "
|
|
265
|
-
"OAuth app, then re-run)"
|
|
266
|
-
)
|
|
267
|
-
return "\n".join(
|
|
268
|
-
f" {row.get('Name')} --connection-id {row.get('Id')}"
|
|
269
|
-
f" (folder: {row.get('Folder')})"
|
|
270
|
-
for row in rows
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def merge_bindings(
|
|
275
|
-
path: str,
|
|
276
|
-
connection: dict,
|
|
277
|
-
connection_name: str,
|
|
278
|
-
folder_name: str,
|
|
279
|
-
) -> tuple[bool, bool]:
|
|
280
|
-
"""Add connection + folder entries to `bindings.json`, in place.
|
|
281
|
-
|
|
282
|
-
ADDITIVE AND IDEMPOTENT. An existing entry under the same symbolic name is
|
|
283
|
-
updated, never duplicated, and every other entry is left exactly as it was —
|
|
284
|
-
`bindings.json` is an authored file that may already carry bindings for
|
|
285
|
-
connectors this run knows nothing about.
|
|
286
|
-
|
|
287
|
-
Returns `(wrote_connection, wrote_folder)` describing what changed, so the
|
|
288
|
-
caller can report the specific thing rather than "updated bindings".
|
|
289
|
-
"""
|
|
290
|
-
import os
|
|
291
|
-
|
|
292
|
-
document: dict = {"schemaVersion": "1", "bindings": []}
|
|
293
|
-
if os.path.exists(path):
|
|
294
|
-
try:
|
|
295
|
-
with open(path, encoding="utf-8") as handle:
|
|
296
|
-
loaded = json.load(handle)
|
|
297
|
-
if isinstance(loaded, dict):
|
|
298
|
-
document = loaded
|
|
299
|
-
document.setdefault("schemaVersion", "1")
|
|
300
|
-
document.setdefault("bindings", [])
|
|
301
|
-
except (OSError, ValueError):
|
|
302
|
-
# A malformed bindings.json is the author's file and not ours to
|
|
303
|
-
# discard: refuse rather than overwrite work we cannot read.
|
|
304
|
-
raise ConnectionError_(
|
|
305
|
-
f"{path} exists but is not readable JSON. Fix or remove it; "
|
|
306
|
-
"this command will not overwrite a file it cannot parse."
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
def upsert(symbolic: str, key: str, attribute: str) -> bool:
|
|
310
|
-
for entry in document["bindings"]:
|
|
311
|
-
if entry.get("name") == symbolic or entry.get("id") == symbolic:
|
|
312
|
-
changed = entry.get("resourceKey") != key
|
|
313
|
-
entry.update({
|
|
314
|
-
"id": entry.get("id", symbolic), "name": symbolic,
|
|
315
|
-
"resource": "Connection", "resourceKey": key,
|
|
316
|
-
"default": key, "propertyAttribute": attribute,
|
|
317
|
-
})
|
|
318
|
-
return changed
|
|
319
|
-
document["bindings"].append({
|
|
320
|
-
"id": symbolic, "name": symbolic, "resource": "Connection",
|
|
321
|
-
"resourceKey": key, "default": key, "propertyAttribute": attribute,
|
|
322
|
-
})
|
|
323
|
-
return True
|
|
324
|
-
|
|
325
|
-
wrote_connection = upsert(connection_name, str(connection["Id"]), "ConnectionId")
|
|
326
|
-
wrote_folder = upsert(folder_name, str(connection["FolderKey"]), "FolderKey")
|
|
327
|
-
|
|
328
|
-
with open(path, "w", encoding="utf-8") as handle:
|
|
329
|
-
json.dump(document, handle, indent=2)
|
|
330
|
-
handle.write("\n")
|
|
331
|
-
return wrote_connection, wrote_folder
|
|
@@ -1,355 +0,0 @@
|
|
|
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
|
-
}
|