@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,299 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Match the object a Generic connector action addresses, deterministically.
|
|
3
|
+
|
|
4
|
+
`prepare --object Company__C` used to mean EXACTLY `Company__C`: the catalog is
|
|
5
|
+
matched by equality, the user's casing is not the API's, and the failure was a
|
|
6
|
+
dead end — "object was not found" with no candidates, against a catalog the
|
|
7
|
+
command had just fetched. The author's recovery was the hand-written JMESPath
|
|
8
|
+
recipe this module exists to retire.
|
|
9
|
+
|
|
10
|
+
THE LADDER, strictest first, stopping at the first rung with EXACTLY one hit:
|
|
11
|
+
|
|
12
|
+
1. exact `Name`;
|
|
13
|
+
2. case-insensitive `Name`;
|
|
14
|
+
3. case-insensitive `DisplayName`.
|
|
15
|
+
|
|
16
|
+
A unique case-insensitive match proceeds LOUDLY (the caller prints the
|
|
17
|
+
normalization) — casing is presentation, so those rungs cannot pick a thing
|
|
18
|
+
the author did not name. SUBSTRING MATCHING NEVER PROCEEDS, unique or not:
|
|
19
|
+
"unique among what exists" is not "unique among what the author meant", and
|
|
20
|
+
the two diverge exactly when it hurts — `--object Company` on a tenant whose
|
|
21
|
+
only Create-capable hit is `DandBCompany` would silently prepare an object the
|
|
22
|
+
author never named, and the same source line would prepare DIFFERENT objects
|
|
23
|
+
on different tenants. So substring hits are how the REFUSAL finds its
|
|
24
|
+
candidates, nothing more; a single candidate costs one confirm-and-re-run
|
|
25
|
+
turn, which is the cheapest insurance in this file.
|
|
26
|
+
|
|
27
|
+
Several matches refuse with the candidate table — a wrong field value is one
|
|
28
|
+
wrong input, a wrong object is a wrong flow, so nothing here ever picks first.
|
|
29
|
+
Zero matches on a multi-word input refuse with the candidates of the most
|
|
30
|
+
selective word, because qualifier words are facets, not name material: "custom
|
|
31
|
+
company" contains-matches nothing as a phrase, "custom" alone matches 26
|
|
32
|
+
unrelated objects on a real Salesforce org, and "company" alone matches the 5
|
|
33
|
+
rows the author actually has to choose between (measured, 2026-09-02).
|
|
34
|
+
|
|
35
|
+
Candidates are narrowed to objects whose `Operations` include this action's
|
|
36
|
+
verb before any matching: an object that cannot Create is never the right
|
|
37
|
+
answer for insert-record, and showing it only lengthens the table.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"ObjectMatchError",
|
|
47
|
+
"LedgerError",
|
|
48
|
+
"match_object",
|
|
49
|
+
"format_candidates",
|
|
50
|
+
"read_ledger",
|
|
51
|
+
"record_object_resolution",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Enough rows to choose from without dumping a catalog into the error. The
|
|
55
|
+
# most-selective-token fallback can legitimately produce dozens of rows; past
|
|
56
|
+
# this many the table stops being a choice and starts being the unfiltered
|
|
57
|
+
# list the ladder exists to avoid.
|
|
58
|
+
MAX_CANDIDATES = 15
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ObjectMatchError(Exception):
|
|
62
|
+
"""No unique object for the requested name, with the candidates a human
|
|
63
|
+
(or an agent with the prompt in hand) needs to choose.
|
|
64
|
+
|
|
65
|
+
`retriable` marks every outcome that fell PAST the name rungs — zero
|
|
66
|
+
matches, and the substring refusals (unique or ambiguous). All of them can
|
|
67
|
+
be caused by a stale `uip is resources list` cache hiding a real object:
|
|
68
|
+
`--object User` on a catalog that lacks `User` but carries `UserAlias`
|
|
69
|
+
would otherwise refuse with a recovery message pointing at the WRONG
|
|
70
|
+
object, and the cache never invalidates itself. The caller refreshes the
|
|
71
|
+
catalog once and re-runs the whole ladder before believing any of them.
|
|
72
|
+
Ambiguity WITHIN a name rung (two objects whose names are case-variants)
|
|
73
|
+
is not retriable — a fresh catalog has the same two names in it.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, message: str, retriable: bool = False):
|
|
77
|
+
super().__init__(message)
|
|
78
|
+
self.retriable = retriable
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _name(row: dict) -> str:
|
|
82
|
+
return str(row.get("Name") or "")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _display(row: dict) -> str:
|
|
86
|
+
return str(row.get("DisplayName") or "")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _supports(row: dict, verb: str | None) -> bool:
|
|
90
|
+
if not verb:
|
|
91
|
+
return True
|
|
92
|
+
operations = row.get("Operations")
|
|
93
|
+
if not isinstance(operations, list):
|
|
94
|
+
# A catalog that does not report operations cannot be narrowed by one;
|
|
95
|
+
# keep the row rather than hiding it on missing data.
|
|
96
|
+
return True
|
|
97
|
+
return verb in operations
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def format_candidates(rows: list[dict]) -> str:
|
|
101
|
+
"""The table an ambiguous match prints: enough columns to choose with.
|
|
102
|
+
|
|
103
|
+
`Custom: yes` is how a prompt's "custom" qualifier resolves — it is a facet
|
|
104
|
+
column here precisely so nothing upstream has to parse it out of English.
|
|
105
|
+
"""
|
|
106
|
+
shown = rows[:MAX_CANDIDATES]
|
|
107
|
+
name_w = max([len("Name"), *(len(_name(r)) for r in shown)])
|
|
108
|
+
disp_w = max([len("DisplayName"), *(len(_display(r)) for r in shown)])
|
|
109
|
+
lines = [
|
|
110
|
+
f" {'Name'.ljust(name_w)} {'DisplayName'.ljust(disp_w)} Custom Operations",
|
|
111
|
+
]
|
|
112
|
+
for row in shown:
|
|
113
|
+
operations = row.get("Operations")
|
|
114
|
+
ops = ",".join(operations) if isinstance(operations, list) else "?"
|
|
115
|
+
lines.append(
|
|
116
|
+
f" {_name(row).ljust(name_w)} {_display(row).ljust(disp_w)} "
|
|
117
|
+
f"{str(row.get('Custom') or '?').ljust(6)} {ops}"
|
|
118
|
+
)
|
|
119
|
+
if len(rows) > MAX_CANDIDATES:
|
|
120
|
+
lines.append(f" … and {len(rows) - MAX_CANDIDATES} more")
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _contains(rows: list[dict], needle: str) -> list[dict]:
|
|
125
|
+
lowered = needle.lower()
|
|
126
|
+
return [
|
|
127
|
+
row for row in rows
|
|
128
|
+
if lowered in _name(row).lower() or lowered in _display(row).lower()
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def match_object(
|
|
133
|
+
rows: list[dict], wanted: str, verb: str | None = None,
|
|
134
|
+
) -> tuple[dict, str | None]:
|
|
135
|
+
"""The catalog row for `wanted`, or raise `ObjectMatchError`.
|
|
136
|
+
|
|
137
|
+
Returns `(row, note)`: `note` is None on an exact match and the
|
|
138
|
+
normalization sentence otherwise, so the caller can print how the given
|
|
139
|
+
name became the API name.
|
|
140
|
+
"""
|
|
141
|
+
wanted = (wanted or "").strip()
|
|
142
|
+
if not wanted:
|
|
143
|
+
raise ObjectMatchError("--object needs a name to match.")
|
|
144
|
+
pool = [row for row in rows if _supports(row, verb)]
|
|
145
|
+
|
|
146
|
+
exact = [row for row in pool if _name(row) == wanted]
|
|
147
|
+
if len(exact) == 1:
|
|
148
|
+
return exact[0], None
|
|
149
|
+
|
|
150
|
+
lowered = wanted.lower()
|
|
151
|
+
for rung, key in (("name", _name), ("display name", _display)):
|
|
152
|
+
hits = [row for row in pool if key(row).lower() == lowered]
|
|
153
|
+
if len(hits) == 1:
|
|
154
|
+
return hits[0], f'matched "{wanted}" -> {_name(hits[0])} (by {rung})'
|
|
155
|
+
if len(hits) > 1:
|
|
156
|
+
raise ObjectMatchError(_ambiguous(wanted, hits, verb))
|
|
157
|
+
|
|
158
|
+
# Substring hits only ever feed the refusal. A unique one is still a guess
|
|
159
|
+
# — the pool it is unique within is tenant state, not the author's intent —
|
|
160
|
+
# so it is offered back as the one candidate to confirm, never taken.
|
|
161
|
+
# Everything from here down is `retriable`: a stale catalog can hide the
|
|
162
|
+
# real object the author named, and these refusals would then recover
|
|
163
|
+
# toward the wrong one.
|
|
164
|
+
hits = _contains(pool, wanted)
|
|
165
|
+
if len(hits) == 1:
|
|
166
|
+
raise ObjectMatchError(
|
|
167
|
+
f'no object is NAMED "{wanted}"'
|
|
168
|
+
+ (f" (of those that support {verb})" if verb else "")
|
|
169
|
+
+ f'. One object contains it — if {_name(hits[0])!r} is the one '
|
|
170
|
+
"the task means, re-run with --object "
|
|
171
|
+
f"{_name(hits[0])}:\n" + format_candidates(hits),
|
|
172
|
+
retriable=True,
|
|
173
|
+
)
|
|
174
|
+
if len(hits) > 1:
|
|
175
|
+
raise ObjectMatchError(_ambiguous(wanted, hits, verb), retriable=True)
|
|
176
|
+
|
|
177
|
+
# Nothing contains the whole input. A multi-word input usually mixes name
|
|
178
|
+
# material with qualifiers; the most selective word's candidates are the
|
|
179
|
+
# smallest non-empty table the author can actually choose from.
|
|
180
|
+
tokens = [t for t in wanted.split() if t]
|
|
181
|
+
if len(tokens) > 1:
|
|
182
|
+
per_token = [(t, _contains(pool, t)) for t in tokens]
|
|
183
|
+
nonempty = [(t, h) for t, h in per_token if h]
|
|
184
|
+
if nonempty:
|
|
185
|
+
token, hits = min(nonempty, key=lambda pair: len(pair[1]))
|
|
186
|
+
raise ObjectMatchError(
|
|
187
|
+
f'no object matches "{wanted}" as one name'
|
|
188
|
+
+ (f" (of those that support {verb})" if verb else "")
|
|
189
|
+
+ f'. Its word "{token}" matches {len(hits)}'
|
|
190
|
+
+ " — qualifiers like \"custom\" are the Custom column below,"
|
|
191
|
+
" not part of the name. Pick one and re-run with"
|
|
192
|
+
" --object <Name>:\n" + format_candidates(hits),
|
|
193
|
+
retriable=True,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
raise ObjectMatchError(
|
|
197
|
+
f'no object in the connected catalog matches "{wanted}"'
|
|
198
|
+
+ (f" and supports {verb}" if verb else "")
|
|
199
|
+
+ f" ({len(pool)} object(s) searched by name, display name and"
|
|
200
|
+
" substring). Check the value against the words the task used,"
|
|
201
|
+
" or list the catalog to see what is there.",
|
|
202
|
+
retriable=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _ambiguous(wanted: str, hits: list[dict], verb: str | None) -> str:
|
|
207
|
+
return (
|
|
208
|
+
f'"{wanted}" matches {len(hits)} objects'
|
|
209
|
+
+ (f" that support {verb}" if verb else "")
|
|
210
|
+
+ ". Nothing here picks one for you: a wrong object is a wrong flow."
|
|
211
|
+
" Pick by the task's wording (Custom: yes marks a tenant-only object),"
|
|
212
|
+
" or present these and let the user choose, then re-run with"
|
|
213
|
+
" --object <Name>:\n" + format_candidates(hits)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class LedgerError(Exception):
|
|
218
|
+
"""The ledger file exists but cannot be read back — refuse, don't clobber."""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def read_ledger(out_dir: str) -> dict:
|
|
222
|
+
"""The whole `resolutions.json` ledger, or raise {@link LedgerError}.
|
|
223
|
+
|
|
224
|
+
A file that will not parse is REFUSED rather than silently reset to `{}` —
|
|
225
|
+
the same rule `bindings.json` follows: a corrupt-but-recoverable audit of
|
|
226
|
+
every resolution prepare ever made is worth more than this run finishing,
|
|
227
|
+
and overwriting it is the one way to make the corruption permanent.
|
|
228
|
+
"""
|
|
229
|
+
path = os.path.join(out_dir, "resolutions.json")
|
|
230
|
+
if not os.path.exists(path):
|
|
231
|
+
return {}
|
|
232
|
+
try:
|
|
233
|
+
with open(path, encoding="utf-8") as handle:
|
|
234
|
+
ledger = json.load(handle)
|
|
235
|
+
except (OSError, ValueError) as error:
|
|
236
|
+
raise LedgerError(
|
|
237
|
+
f"{path} exists but cannot be read back ({error}); refusing to "
|
|
238
|
+
"overwrite it. Fix or remove the file and re-run."
|
|
239
|
+
) from error
|
|
240
|
+
if not isinstance(ledger, dict):
|
|
241
|
+
# Parseable-but-not-a-dict (a top-level array, a bare string) is
|
|
242
|
+
# corruption wearing valid JSON; sliding it past the refusal above
|
|
243
|
+
# would overwrite whatever it used to be.
|
|
244
|
+
raise LedgerError(
|
|
245
|
+
f"{path} exists but is not a JSON object "
|
|
246
|
+
f"(found {type(ledger).__name__}); refusing to overwrite it. "
|
|
247
|
+
"Fix or remove the file and re-run."
|
|
248
|
+
)
|
|
249
|
+
return ledger
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def record_object_resolution(
|
|
253
|
+
out_dir: str, node_type: str, requested: str, row: dict, verb: str | None,
|
|
254
|
+
) -> str:
|
|
255
|
+
"""Record the match in the overlay ledger, as the FIRST resolution.
|
|
256
|
+
|
|
257
|
+
The run artifact (`bindings.json` + `resolutions.json`) is a complete,
|
|
258
|
+
diffable audit of everything prepare decided — except this decision, which
|
|
259
|
+
lived only in stdout. The row lands in a sibling `objects` section rather
|
|
260
|
+
than in `resolutions`: an object identity is not a field lookup, and it
|
|
261
|
+
should not wear a fake `field` key. Keys NEST (`objects[nodeType][given]`)
|
|
262
|
+
rather than encode a tuple: the lookup rows' tuple-string keys exist to
|
|
263
|
+
match a JavaScript `JSON.stringify` that reads them back, and nothing in
|
|
264
|
+
JavaScript reads this section — a hand-maintained "matches JSON.stringify"
|
|
265
|
+
encoding with no reader on the other side is exactly how the #660
|
|
266
|
+
resolution-key bug happened.
|
|
267
|
+
|
|
268
|
+
Unlike lookup rows, the current match is ALWAYS written, `--refresh` or
|
|
269
|
+
not. A lookup row stands because re-resolving costs a network call and can
|
|
270
|
+
silently change an artifact; this row costs nothing to recompute and MUST
|
|
271
|
+
agree with the overlay written beside it in this same run — an old row
|
|
272
|
+
kept "for stability" would describe a schema this run just replaced.
|
|
273
|
+
The ledger records the DECISION only; the schema it selected lives in the
|
|
274
|
+
overlay, never duplicated here.
|
|
275
|
+
|
|
276
|
+
The file's sections are additive under `schemaVersion: "1"`: readers
|
|
277
|
+
ignore keys they do not know (the JS resolutions loader reads only
|
|
278
|
+
`.resolutions`), and any change to the MEANING of an existing section
|
|
279
|
+
bumps the version.
|
|
280
|
+
"""
|
|
281
|
+
ledger = read_ledger(out_dir)
|
|
282
|
+
objects = ledger.get("objects") or {}
|
|
283
|
+
per_type = objects.get(node_type) or {}
|
|
284
|
+
per_type[requested] = {
|
|
285
|
+
"value": _name(row),
|
|
286
|
+
"displayName": _display(row),
|
|
287
|
+
"custom": str(row.get("Custom") or "").strip().lower() in ("yes", "true", "1"),
|
|
288
|
+
**({"operation": verb} if verb else {}),
|
|
289
|
+
}
|
|
290
|
+
objects[node_type] = per_type
|
|
291
|
+
ledger["objects"] = objects
|
|
292
|
+
ledger["resolutions"] = ledger.get("resolutions") or {}
|
|
293
|
+
ledger["schemaVersion"] = "1"
|
|
294
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
295
|
+
path = os.path.join(out_dir, "resolutions.json")
|
|
296
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
297
|
+
json.dump(ledger, handle, indent=2, sort_keys=True)
|
|
298
|
+
handle.write("\n")
|
|
299
|
+
return path
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Resolve a connector lookup field against a live connection.
|
|
3
|
+
|
|
4
|
+
`prepare --resolve channel:profile.email=dustin@example.com` turns a
|
|
5
|
+
human-meaningful value into the opaque id the connector wants, and records the
|
|
6
|
+
answer so `compile` can read it offline. This module is the retrieval half.
|
|
7
|
+
|
|
8
|
+
WHY IT LIVES HERE AND NOT IN COMPILE. Resolution needs the network; compile must
|
|
9
|
+
not have it. The eval harness runs `check` and `compile` with no `uip` login,
|
|
10
|
+
and the byte-lock gates require that the same source produces the same bytes —
|
|
11
|
+
a compile that made live calls would make those gates depend on tenant state and
|
|
12
|
+
they would stop meaning anything. So the expensive call happens once, in an
|
|
13
|
+
explicit command, and its result lands in a file that is diffable and reviewable.
|
|
14
|
+
|
|
15
|
+
THE STRATEGY IS DATA, NOT CODE. `reference` says how to retrieve the collection:
|
|
16
|
+
a `filterPattern` means one request; its absence means paging; `childPath` means
|
|
17
|
+
a tree; `dependsOn` means another field must resolve first. That is why this is a
|
|
18
|
+
generator reading metadata rather than 103 hand-written connector helpers.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ResolutionError",
|
|
29
|
+
"parse_resolve_flag",
|
|
30
|
+
"resolve_one",
|
|
31
|
+
"order_resolutions",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# A scan has to stop somewhere, and the caps must be set against the MEASURED
|
|
35
|
+
# page size rather than a guess. Integration Service serves **1,000 records per
|
|
36
|
+
# page** for a curated collection (verified against a live Slack connection:
|
|
37
|
+
# `curated_channels` returns 1000 with `HasMore: "true"`).
|
|
38
|
+
#
|
|
39
|
+
# The first version of this capped at 400 records — a number chosen for how many
|
|
40
|
+
# rows a person might scroll, which is the wrong unit entirely. It meant the loop
|
|
41
|
+
# broke partway through page ONE and never paginated at all. The eval task built
|
|
42
|
+
# to exercise this puts its target channel on page 3 (record ~2,001), so the
|
|
43
|
+
# resolver reported "not found" for a channel that is plainly there, and the
|
|
44
|
+
# agent fell back to hand-rolling the paging this exists to do for it.
|
|
45
|
+
#
|
|
46
|
+
# 25 pages is ~30s of requests in the worst case, spent once, in an explicit
|
|
47
|
+
# command whose whole job is this. A collection with a `filterPattern` never
|
|
48
|
+
# reaches the scan at all.
|
|
49
|
+
PAGE_SIZE_HINT = 1000
|
|
50
|
+
MAX_SCANNED = 25 * PAGE_SIZE_HINT
|
|
51
|
+
PAGE_GUARD = 50
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResolutionError(Exception):
|
|
55
|
+
"""A lookup that could not be resolved, with the reason a human needs."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_resolve_flag(raw: str) -> tuple[str, str, str]:
|
|
59
|
+
"""`channel:profile.email=dustin@example.com` -> (field, by, value).
|
|
60
|
+
|
|
61
|
+
Split on the FIRST colon and the FIRST `=`. Both halves can legitimately
|
|
62
|
+
contain the other delimiter — `by` is a dotted path and a value can be
|
|
63
|
+
anything a person typed — so a greedy split silently mis-parses real input.
|
|
64
|
+
"""
|
|
65
|
+
field, sep, rest = raw.partition(":")
|
|
66
|
+
if not sep or not field:
|
|
67
|
+
raise ResolutionError(
|
|
68
|
+
f"--resolve {raw!r}: expected <field>:<by>=<value>, "
|
|
69
|
+
"e.g. --resolve channel:profile.email=someone@example.com"
|
|
70
|
+
)
|
|
71
|
+
by, sep, value = rest.partition("=")
|
|
72
|
+
if not sep or not by:
|
|
73
|
+
raise ResolutionError(
|
|
74
|
+
f"--resolve {raw!r}: expected <field>:<by>=<value>, "
|
|
75
|
+
f"but found no '=' after the search field {by or rest!r}."
|
|
76
|
+
)
|
|
77
|
+
return field, by, value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def order_resolutions(
|
|
81
|
+
requested: list[tuple[str, str, str]], specs: dict,
|
|
82
|
+
) -> list[tuple[str, str, str]]:
|
|
83
|
+
"""Order the requests so a `dependsOn` prerequisite resolves first.
|
|
84
|
+
|
|
85
|
+
27 fields in the shipped library are not addressable until another field is
|
|
86
|
+
known — Jira's issue type needs the project, Amazon Connect's phone number
|
|
87
|
+
needs the instance. Resolving in the order the flags were typed leaves an
|
|
88
|
+
unsubstituted `{placeholder}` in a URL, and the error the connector returns
|
|
89
|
+
for that names neither field.
|
|
90
|
+
"""
|
|
91
|
+
by_field = {field: (field, by, value) for field, by, value in requested}
|
|
92
|
+
ordered: list[tuple[str, str, str]] = []
|
|
93
|
+
placed: set[str] = set()
|
|
94
|
+
|
|
95
|
+
def place(field: str, trail: tuple[str, ...] = ()) -> None:
|
|
96
|
+
if field in placed:
|
|
97
|
+
return
|
|
98
|
+
if field in trail:
|
|
99
|
+
cycle = " -> ".join([*trail, field])
|
|
100
|
+
raise ResolutionError(f"circular dependsOn among lookup fields: {cycle}")
|
|
101
|
+
for prerequisite in (specs.get(field, {}).get("dependsOn") or []):
|
|
102
|
+
if prerequisite in by_field:
|
|
103
|
+
place(prerequisite, (*trail, field))
|
|
104
|
+
else:
|
|
105
|
+
raise ResolutionError(
|
|
106
|
+
f"cannot resolve {field!r}: its collection is addressed through "
|
|
107
|
+
f"{prerequisite!r}, which has no --resolve of its own. Add "
|
|
108
|
+
f"--resolve {prerequisite}:<by>=<value> and re-run."
|
|
109
|
+
)
|
|
110
|
+
placed.add(field)
|
|
111
|
+
ordered.append(by_field[field])
|
|
112
|
+
|
|
113
|
+
for field, _by, _value in requested:
|
|
114
|
+
place(field)
|
|
115
|
+
return ordered
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _envelope_items(payload: dict) -> list:
|
|
119
|
+
"""The records out of a `uip is resources run list` reply.
|
|
120
|
+
|
|
121
|
+
The reply is `{"Result": "Success", "Code": ..., "Data": {"items": [...],
|
|
122
|
+
"Pagination": {...}}}` — records live under `Data`, and `Result` is the
|
|
123
|
+
STATUS STRING, not a container. Reading a top-level `items` finds nothing
|
|
124
|
+
and reports "scanned 0 records" against a collection that is full, which is
|
|
125
|
+
exactly what this did until an eval run walked it into the real CLI.
|
|
126
|
+
"""
|
|
127
|
+
data = payload.get("Data")
|
|
128
|
+
if isinstance(data, dict) and isinstance(data.get("items"), list):
|
|
129
|
+
return data["items"]
|
|
130
|
+
if isinstance(payload.get("items"), list):
|
|
131
|
+
return payload["items"]
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _envelope_pagination(payload: dict) -> dict:
|
|
136
|
+
"""The `Pagination` block, from under `Data`."""
|
|
137
|
+
data = payload.get("Data")
|
|
138
|
+
if isinstance(data, dict) and isinstance(data.get("Pagination"), dict):
|
|
139
|
+
return data["Pagination"]
|
|
140
|
+
if isinstance(payload.get("Pagination"), dict):
|
|
141
|
+
return payload["Pagination"]
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _truthy(value: object) -> bool:
|
|
146
|
+
"""`HasMore` arrives as the STRING "false" as often as the boolean.
|
|
147
|
+
|
|
148
|
+
`if payload["Pagination"]["HasMore"]` on the string "false" is True, which
|
|
149
|
+
pages forever on a collection that just said it has no more.
|
|
150
|
+
"""
|
|
151
|
+
if isinstance(value, str):
|
|
152
|
+
return value.strip().lower() in ("true", "1", "yes")
|
|
153
|
+
return bool(value)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _uip_list(connector_key: str, collection: str, connection_id: str,
|
|
157
|
+
query: str | None) -> dict:
|
|
158
|
+
"""One `uip is resources run list` page, as parsed JSON."""
|
|
159
|
+
command = [
|
|
160
|
+
"uip", "is", "resources", "run", "list", connector_key, collection,
|
|
161
|
+
"--connection-id", connection_id, "--output", "json",
|
|
162
|
+
]
|
|
163
|
+
if query:
|
|
164
|
+
command += ["--query", query]
|
|
165
|
+
result = subprocess.run(command, capture_output=True, text=True)
|
|
166
|
+
if result.returncode != 0:
|
|
167
|
+
# `uip` prints a self-update failure to stderr before its real output,
|
|
168
|
+
# so stderr being non-empty is not evidence of anything. Judge the exit
|
|
169
|
+
# code and the payload.
|
|
170
|
+
raise ResolutionError(
|
|
171
|
+
f"listing {collection} failed: {result.stderr.strip() or 'no output'}"
|
|
172
|
+
)
|
|
173
|
+
try:
|
|
174
|
+
return json.loads(result.stdout)
|
|
175
|
+
except ValueError as error:
|
|
176
|
+
raise ResolutionError(
|
|
177
|
+
f"listing {collection} returned output that is not JSON: {error}"
|
|
178
|
+
) from error
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _read_path(record: dict, dotted: str):
|
|
182
|
+
"""`profile.email` out of a record, or None."""
|
|
183
|
+
cursor = record
|
|
184
|
+
for part in dotted.split("."):
|
|
185
|
+
if not isinstance(cursor, dict):
|
|
186
|
+
return None
|
|
187
|
+
cursor = cursor.get(part)
|
|
188
|
+
return cursor
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _substitute(template: str, resolved: dict[str, str]) -> str:
|
|
192
|
+
"""Fill `{fields.project.key}` placeholders from already-resolved fields."""
|
|
193
|
+
out = template
|
|
194
|
+
for field, value in resolved.items():
|
|
195
|
+
out = out.replace("{" + field + "}", str(value))
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def resolve_one(
|
|
200
|
+
connector_key: str,
|
|
201
|
+
spec: dict,
|
|
202
|
+
field: str,
|
|
203
|
+
by: str,
|
|
204
|
+
value: str,
|
|
205
|
+
connection_id: str,
|
|
206
|
+
resolved: dict[str, str],
|
|
207
|
+
) -> str:
|
|
208
|
+
"""The connector-facing id for one lookup, or raise `ResolutionError`.
|
|
209
|
+
|
|
210
|
+
`resolved` carries the values of fields resolved earlier in this run, which
|
|
211
|
+
is what makes a `dependsOn` path addressable.
|
|
212
|
+
"""
|
|
213
|
+
if by not in (spec.get("by") or []):
|
|
214
|
+
searchable = ", ".join(repr(b) for b in (spec.get("by") or []))
|
|
215
|
+
raise ResolutionError(
|
|
216
|
+
f"{field}: cannot search by {by!r}. Searchable by {searchable or '(nothing declared)'}."
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
path = _substitute(spec.get("path") or "", resolved)
|
|
220
|
+
if "{" in path:
|
|
221
|
+
missing = path[path.index("{") + 1: path.index("}")] if "}" in path else "?"
|
|
222
|
+
raise ResolutionError(
|
|
223
|
+
f"{field}: the collection path still contains {{{missing}}}. "
|
|
224
|
+
f"Resolve {missing!r} first with --resolve {missing}:<by>=<value>."
|
|
225
|
+
)
|
|
226
|
+
collection = path.split("?", 1)[0].strip("/")
|
|
227
|
+
query = path.partition("?")[2] or None
|
|
228
|
+
send = spec.get("value") or "id"
|
|
229
|
+
|
|
230
|
+
# A server-side filter turns the whole scan into one request. Where the
|
|
231
|
+
# metadata declares one, use it; where it does not, page.
|
|
232
|
+
filter_pattern = spec.get("filterPattern")
|
|
233
|
+
if filter_pattern:
|
|
234
|
+
filtered = filter_pattern.replace("{filter}", value)
|
|
235
|
+
query = f"{query}&{filtered}" if query else filtered
|
|
236
|
+
|
|
237
|
+
scanned = 0
|
|
238
|
+
pages = 0
|
|
239
|
+
while True:
|
|
240
|
+
payload = _uip_list(connector_key, collection, connection_id, query)
|
|
241
|
+
items = _envelope_items(payload)
|
|
242
|
+
for record in items:
|
|
243
|
+
scanned += 1
|
|
244
|
+
if str(_read_path(record, by)) == value:
|
|
245
|
+
got = _read_path(record, send)
|
|
246
|
+
if got is None:
|
|
247
|
+
raise ResolutionError(
|
|
248
|
+
f"{field}: matched a record on {by}={value!r}, but it "
|
|
249
|
+
f"carries no {send!r} to send."
|
|
250
|
+
)
|
|
251
|
+
return str(got)
|
|
252
|
+
pagination = _envelope_pagination(payload)
|
|
253
|
+
token = pagination.get("NextPageToken")
|
|
254
|
+
if not _truthy(pagination.get("HasMore")) or not token or scanned >= MAX_SCANNED:
|
|
255
|
+
break
|
|
256
|
+
pages += 1
|
|
257
|
+
if pages > PAGE_GUARD:
|
|
258
|
+
break
|
|
259
|
+
# Exactly `nextPage`. Guessing `page` / `pageToken` / `nextPageToken`
|
|
260
|
+
# is a documented way to loop forever on page one.
|
|
261
|
+
query = f"{query}&nextPage={token}" if query else f"nextPage={token}"
|
|
262
|
+
|
|
263
|
+
capped = scanned >= MAX_SCANNED or pages > PAGE_GUARD
|
|
264
|
+
raise ResolutionError(
|
|
265
|
+
f"{field}: no record in {collection!r} has {by}={value!r} "
|
|
266
|
+
f"(scanned {scanned} record(s) across {pages + 1} page(s)"
|
|
267
|
+
+ (f"; STOPPED AT THE {MAX_SCANNED}-record cap, so the value may be "
|
|
268
|
+
f"further in" if capped else "")
|
|
269
|
+
+ ").\n"
|
|
270
|
+
f"Check the value, or list the collection to see what is there:\n"
|
|
271
|
+
f" uip is resources run list {connector_key} {collection} "
|
|
272
|
+
f"--connection-id {connection_id}"
|
|
273
|
+
)
|