@uipath/maestro-builder-sdk 6.4.0 → 6.5.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/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,114 +0,0 @@
|
|
|
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"]
|
|
@@ -1,185 +0,0 @@
|
|
|
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
|
|
@@ -1,299 +0,0 @@
|
|
|
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
|