@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,1441 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Convert the canonical JSON connector library into a markdown library.
|
|
3
|
+
|
|
4
|
+
Reads `<source>/index.json` and walks every entry — including the
|
|
5
|
+
`generic/<object>/<op>@<ver>.json` entries the Generic CRUD expansion
|
|
6
|
+
materializes — emitting a parallel tree of `.md` files plus a
|
|
7
|
+
sibling `<output>/index.json` whose `path` fields point at the new
|
|
8
|
+
`.md` files.
|
|
9
|
+
|
|
10
|
+
The output tree is the shape coding agents read for connector
|
|
11
|
+
discovery: header → connector / verb / object / HTTP → required
|
|
12
|
+
inputs → optional inputs → outputs. The authoring agent's connector
|
|
13
|
+
guidance (discovery pattern + Generic CRUD convention) lives in the main
|
|
14
|
+
SKILL.md, not here — the per-op files stay focused on schema.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 convert_library_to_md.py --source <json-lib-dir> --output <md-lib-dir>
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import shutil
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
# Symbol names cited below (`connector(SendMessageToUser, …)`, `.byEmail`) are
|
|
30
|
+
# DERIVED, not stored — and derived by the descriptor generator. Both import the
|
|
31
|
+
# same module so this page cannot cite a symbol that module did not emit.
|
|
32
|
+
from _naming import by_aliases, symbol_index
|
|
33
|
+
from _lookups import classify, load_invariant_collections, reference_of
|
|
34
|
+
# The EXAMPLE is type-checked against the generated descriptor, so its required
|
|
35
|
+
# set has to be the descriptor's, not this module's. The two vocabularies differ —
|
|
36
|
+
# see example_lines.
|
|
37
|
+
from generate_connectors_ts import (
|
|
38
|
+
input_fields as descriptor_input_fields,
|
|
39
|
+
is_container_part,
|
|
40
|
+
mark_lookup_fields as descriptor_mark_lookups,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def enum_value(v: object) -> str:
|
|
47
|
+
"""One enum choice, as the literal an author would actually write.
|
|
48
|
+
|
|
49
|
+
The two schemas encode choices differently: ``inputSchema`` uses plain
|
|
50
|
+
strings (``"Prospect"``), while ``inputDefinition`` uses objects carrying a
|
|
51
|
+
display name alongside the wire value
|
|
52
|
+
(``{"name": "Abu Dhabi - Arabian Standard Time", "value": "Arabian Standard Time"}``).
|
|
53
|
+
Take ``value`` — that is what the flow has to send — so the rendering stays
|
|
54
|
+
identical across both shapes instead of leaking `{'value': …}` into the
|
|
55
|
+
catalog the agent reads.
|
|
56
|
+
"""
|
|
57
|
+
if isinstance(v, dict):
|
|
58
|
+
return str(v.get("value", v.get("name", "")))
|
|
59
|
+
return str(v)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def reference_collection(f: dict) -> str:
|
|
63
|
+
"""The collection to call to resolve a field's value, or "".
|
|
64
|
+
|
|
65
|
+
Prefer `reference.path` over `reference.objectName`: the schema documents
|
|
66
|
+
`path` as the one "used for making the call to get the reference details",
|
|
67
|
+
and the two genuinely disagree. Data Service `entityName` declares
|
|
68
|
+
`objectName: "system"` but is served from `/standard-objects`, so building
|
|
69
|
+
the call from `objectName` sends an author to a collection that is not the
|
|
70
|
+
one holding the records.
|
|
71
|
+
|
|
72
|
+
Either side can carry a filter (`curated_channels?types=...`), and
|
|
73
|
+
`uip is resources run list` takes the collection positionally with the
|
|
74
|
+
filter as `--query`, so the two halves are split here rather than left for
|
|
75
|
+
a reader to guess.
|
|
76
|
+
"""
|
|
77
|
+
reference = f.get("reference")
|
|
78
|
+
if not isinstance(reference, dict):
|
|
79
|
+
return ""
|
|
80
|
+
source = reference.get("path") or reference.get("objectName") or ""
|
|
81
|
+
if not isinstance(source, str) or not source:
|
|
82
|
+
return ""
|
|
83
|
+
return source.split("?", 1)[0].strip("/")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def reference_query(f: dict) -> str:
|
|
87
|
+
"""The filter half of whichever side `reference_collection` used, or ""."""
|
|
88
|
+
reference = f.get("reference")
|
|
89
|
+
if not isinstance(reference, dict):
|
|
90
|
+
return ""
|
|
91
|
+
source = reference.get("path") or reference.get("objectName") or ""
|
|
92
|
+
if not isinstance(source, str):
|
|
93
|
+
return ""
|
|
94
|
+
_, _, query = source.partition("?")
|
|
95
|
+
return query
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def authoring_field(f: dict) -> tuple[str, str, str]:
|
|
99
|
+
"""Return the source-facing name/type and any distinct wire field name.
|
|
100
|
+
|
|
101
|
+
A top-level ``users[*]`` declaration is flattened wire notation. The
|
|
102
|
+
generated TypeScript descriptor exposes it as ``users: string[]``. Showing
|
|
103
|
+
the raw name as a scalar led authors to pass a comma-delimited
|
|
104
|
+
``'users[*]'`` value that compiled but was not a valid multiselect.
|
|
105
|
+
"""
|
|
106
|
+
wire_name = str(f.get("name", "?"))
|
|
107
|
+
type_ = str(f.get("type", "?"))
|
|
108
|
+
if wire_name.endswith("[*]") and "." not in wire_name:
|
|
109
|
+
return wire_name[:-3], f"{type_}[]", wire_name
|
|
110
|
+
return wire_name, type_, ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def enumeration_command(connector_key: str, f: dict) -> str:
|
|
114
|
+
"""The `resources run list` call that shows what a field accepts.
|
|
115
|
+
|
|
116
|
+
Retained ONLY for identity lookups. For an ordinary lookup the helper
|
|
117
|
+
replaced this call, and leaving it on the page would offer two routes to the
|
|
118
|
+
same thing — the reader takes the one they already know, which is the manual
|
|
119
|
+
loop this whole surface exists to remove.
|
|
120
|
+
|
|
121
|
+
An identity lookup has no helper and no offline vocabulary (not one of them
|
|
122
|
+
carries an `enum`, measured across the shipped library), so removing the
|
|
123
|
+
command here would leave the field less documented than before. It is
|
|
124
|
+
reworded rather than deleted: not "how to resolve a name to an id" but "how
|
|
125
|
+
to see what this field accepts".
|
|
126
|
+
"""
|
|
127
|
+
collection = reference_collection(f)
|
|
128
|
+
query = reference_query(f)
|
|
129
|
+
command = (
|
|
130
|
+
f"uip is resources run list {connector_key} {collection} "
|
|
131
|
+
f"--connection-id <id>"
|
|
132
|
+
)
|
|
133
|
+
if query:
|
|
134
|
+
command += f' --query "{query}"'
|
|
135
|
+
return command
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def lookup_lines(
|
|
139
|
+
connector_key: str, action: str, fields: list[dict], symbol: str | None,
|
|
140
|
+
invariant: set,
|
|
141
|
+
) -> list[str]:
|
|
142
|
+
"""The `## Resolving lookups` body: helpers first, commands only where owed.
|
|
143
|
+
|
|
144
|
+
An author reading this page has to turn something a human said ("send it to
|
|
145
|
+
Dustin") into the id the API wants. The old answer here was a `resources run
|
|
146
|
+
list` command and a manual paging loop. The answer now is a helper call and
|
|
147
|
+
one `prepare`, because the id is the wrong thing to write down: it is
|
|
148
|
+
meaningless in review and silently wrong after a move to another tenant.
|
|
149
|
+
|
|
150
|
+
`symbol` is the generated descriptor's exported name, or None when the
|
|
151
|
+
operation has no descriptor (a Generic CRUD expansion) — in which case the
|
|
152
|
+
string form of `lookup()` is what the example shows.
|
|
153
|
+
"""
|
|
154
|
+
resolvable = [(f, reference_of(f)) for f in fields
|
|
155
|
+
if classify(f, invariant) == "lookup"]
|
|
156
|
+
identity = [f for f in fields if classify(f, invariant) == "identity"]
|
|
157
|
+
if not resolvable and not identity:
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
out: list[str] = []
|
|
161
|
+
if resolvable:
|
|
162
|
+
first_field, first_reference = resolvable[0]
|
|
163
|
+
# The lookup table is keyed by the WIRE name (`users[*]`), the same
|
|
164
|
+
# string `--resolve` and buildConnectorInputs match on. The table's
|
|
165
|
+
# `field` column keeps the authoring name, which is what the reader
|
|
166
|
+
# writes as an object key.
|
|
167
|
+
first_name = str(first_field.get("name") or authoring_field(first_field)[0])
|
|
168
|
+
first_names = [str(n) for n in (first_reference.get("lookupNames") or [])]
|
|
169
|
+
first_aliases = by_aliases(first_names)
|
|
170
|
+
# Without a descriptor the string form carries the real key AND action;
|
|
171
|
+
# a `<action>` placeholder here was a copy-paste trap, not a hint.
|
|
172
|
+
holder = symbol or f"'{connector_key}', '{action}'"
|
|
173
|
+
call = f"lookup({holder}, '{first_name}')"
|
|
174
|
+
sample = (f".{next(iter(first_aliases.values()))}('…')" if first_aliases
|
|
175
|
+
else f".by('{first_names[0]}', '…')" if first_names else ".by('id', '…')")
|
|
176
|
+
# The MECHANISM — resolve, never hand-write the wire value — is true of
|
|
177
|
+
# every lookup. The old JUSTIFICATION was not: it asserted the value is
|
|
178
|
+
# an opaque, connection-specific id, which is false for 320 of the
|
|
179
|
+
# library's 2,563 rows and, on `uipath-salesforce-slack`
|
|
180
|
+
# `send-message-to-user`, for a row printed three lines below the claim
|
|
181
|
+
# (`send_as` sends the word `bot`) — #706.
|
|
182
|
+
#
|
|
183
|
+
# Nothing in the metadata says which case a field is in: a reference
|
|
184
|
+
# block carries only childPath, dependsOn, filterPattern, hydration,
|
|
185
|
+
# lookupNames, lookupValue, objectName and path. Classifying would mean
|
|
186
|
+
# guessing from the SPELLING of `lookupValue`, a list that goes stale
|
|
187
|
+
# the next time a connector ships a handle nobody thought of. So the id
|
|
188
|
+
# claim is stated as the CONDITION it always was, and the `sends` column
|
|
189
|
+
# — already printed below — answers which case this page is.
|
|
190
|
+
out.append(
|
|
191
|
+
"These fields send a value the connector defines, not the label you "
|
|
192
|
+
"know it by — the `sends` column says which. Do not write that value "
|
|
193
|
+
"by hand: where it is an opaque id it is also connection-specific, so "
|
|
194
|
+
"a pasted one resolves to nobody (or to somebody else) against a "
|
|
195
|
+
"different connection. Resolve it late:"
|
|
196
|
+
)
|
|
197
|
+
out.append("")
|
|
198
|
+
out.append("```ts")
|
|
199
|
+
# A dotted wire name is a legal object key only when quoted.
|
|
200
|
+
# `buildConnectorInputs` accepts the flat form as well as the nested
|
|
201
|
+
# one, so this stays a one-line illustration rather than growing into a
|
|
202
|
+
# second copy of the example below.
|
|
203
|
+
key = first_name if _IDENT.fullmatch(first_name) else json.dumps(first_name)
|
|
204
|
+
out.append(f"{key}: {call}{sample}")
|
|
205
|
+
out.append("```")
|
|
206
|
+
out.append("")
|
|
207
|
+
out.append("| field | helpers | matches on | sends |")
|
|
208
|
+
out.append("| --- | --- | --- | --- |")
|
|
209
|
+
for f, reference in resolvable:
|
|
210
|
+
name, _, _ = authoring_field(f)
|
|
211
|
+
names = [str(n) for n in (reference.get("lookupNames") or [])]
|
|
212
|
+
aliases = by_aliases(names)
|
|
213
|
+
helper_cell = (" ".join(f"`.{a}(v)`" for a in aliases.values())
|
|
214
|
+
if aliases else "_(generic form only)_")
|
|
215
|
+
# `objectName` can carry a query string
|
|
216
|
+
# (`curated_channels?types=public_channel`). Show the collection.
|
|
217
|
+
target = (str(reference.get("objectName") or "").split("?", 1)[0]
|
|
218
|
+
or reference_collection(f))
|
|
219
|
+
out.append(
|
|
220
|
+
f"| `{name}` | {helper_cell} | "
|
|
221
|
+
+ " ".join(f"`{n}`" for n in names)
|
|
222
|
+
+ f" | `{target}.{reference.get('lookupValue')}` |"
|
|
223
|
+
)
|
|
224
|
+
out.append("")
|
|
225
|
+
out.append("Any field also takes the generic form: `.by('<name>', value)`.")
|
|
226
|
+
out.append("")
|
|
227
|
+
# ORDERING, not just the command. "Resolve before compiling" read as
|
|
228
|
+
# "resolve now", and agents ran this prepare before a line of source
|
|
229
|
+
# existed (observed across four sessions). The page's job is the
|
|
230
|
+
# loop: author the token, let check name the command, run it once.
|
|
231
|
+
out.append(
|
|
232
|
+
"Author the `lookup()` call and finish the flow — `check` reports "
|
|
233
|
+
"each token still unresolved with exactly this command; run it "
|
|
234
|
+
"once before compiling (`compile` refuses an unresolved lookup):"
|
|
235
|
+
)
|
|
236
|
+
out.append("")
|
|
237
|
+
# ONE command carrying every --resolve. `prepare` calls accumulate into
|
|
238
|
+
# the same overlay, but a page printing one command per field reads as
|
|
239
|
+
# one round-trip per field, and the whole point is that it is one.
|
|
240
|
+
out.append("```bash")
|
|
241
|
+
# NO --connection-id. `prepare` discovers the connection itself and
|
|
242
|
+
# writes both keys into bindings.json. Showing the flag is what kept
|
|
243
|
+
# agents listing connections by hand first — measured 3/3 runs after the
|
|
244
|
+
# discovery landed, because every documented example still had it.
|
|
245
|
+
out.append(
|
|
246
|
+
f"npx flow-sdk registry prepare {connector_key} {action} \\"
|
|
247
|
+
)
|
|
248
|
+
resolves = []
|
|
249
|
+
for f, reference in resolvable:
|
|
250
|
+
name, _, _ = authoring_field(f)
|
|
251
|
+
names = [str(n) for n in (reference.get("lookupNames") or [])] or ["id"]
|
|
252
|
+
resolves.append(f" --resolve {name}:{names[0]}=<value>")
|
|
253
|
+
out.extend(line + " \\" for line in resolves[:-1])
|
|
254
|
+
out.append(resolves[-1])
|
|
255
|
+
out.append("```")
|
|
256
|
+
out.append("")
|
|
257
|
+
strategies = {reference_strategy(r) for _, r in resolvable}
|
|
258
|
+
out.append(f"Strategy: {'; '.join(sorted(strategies))}.")
|
|
259
|
+
out.append("")
|
|
260
|
+
|
|
261
|
+
if identity:
|
|
262
|
+
out.append(
|
|
263
|
+
"**These fields take a plain string.** Their `reference` enumerates "
|
|
264
|
+
"the legal values rather than mapping a name to an id — what you "
|
|
265
|
+
"send back is what you searched for — so there is no helper and "
|
|
266
|
+
"nothing to resolve. The values exist only behind the connection, "
|
|
267
|
+
"so this is how to see what is accepted:"
|
|
268
|
+
)
|
|
269
|
+
out.append("")
|
|
270
|
+
out.append("```bash")
|
|
271
|
+
for f in identity:
|
|
272
|
+
name, _, _ = authoring_field(f)
|
|
273
|
+
out.append(f"# {name}")
|
|
274
|
+
out.append(enumeration_command(connector_key, f))
|
|
275
|
+
out.append("```")
|
|
276
|
+
out.append("")
|
|
277
|
+
return out
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def reference_strategy(reference: dict) -> str:
|
|
281
|
+
"""Prose for how `prepare` will retrieve this collection."""
|
|
282
|
+
if reference.get("dependsOn"):
|
|
283
|
+
depends = ", ".join(f"`{d}`" for d in reference["dependsOn"])
|
|
284
|
+
return f"dependent — resolve {depends} first"
|
|
285
|
+
if reference.get("filterPattern"):
|
|
286
|
+
return "one request (server-side filter)"
|
|
287
|
+
if reference.get("childPath"):
|
|
288
|
+
return "hierarchical walk"
|
|
289
|
+
return "paged scan (no server-side filter declared)"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def example_value(
|
|
293
|
+
f: dict, symbol: str | None, invariant: set,
|
|
294
|
+
connector_key: str = "", action: str = "",
|
|
295
|
+
) -> str:
|
|
296
|
+
"""The value to show for one required input.
|
|
297
|
+
|
|
298
|
+
Three rules, in order, and each is a decision rather than a detail:
|
|
299
|
+
|
|
300
|
+
* a resolvable lookup shows its helper — that is the whole point of the
|
|
301
|
+
helper existing, and an example that pasted an id would teach the opposite
|
|
302
|
+
of what `## Resolving lookups` says two sections above;
|
|
303
|
+
* an enum shows its FIRST DECLARED CHOICE, because that is a real legal
|
|
304
|
+
value and it shows the vocabulary at the same time;
|
|
305
|
+
* anything else shows `input('<name>')`, not a fake literal. A generated
|
|
306
|
+
`'<summary>'` compiles and is a lie; `input('summary')` compiles, is
|
|
307
|
+
idiomatic, and is honestly a placeholder. Copying it yields a flow that
|
|
308
|
+
declares the input rather than one carrying invented data.
|
|
309
|
+
"""
|
|
310
|
+
name, _, _ = authoring_field(f)
|
|
311
|
+
# The lookup table is keyed by the WIRE name — `addLabelIds[*]`, not the
|
|
312
|
+
# authoring name `addLabelIds` — because that is what `--resolve` and
|
|
313
|
+
# `buildConnectorInputs` both match on. Passing the authoring name here is a
|
|
314
|
+
# type error at the `lookup(...)` call, which is how it was found.
|
|
315
|
+
wire = str(f.get("name") or name)
|
|
316
|
+
kind = classify(f, invariant)
|
|
317
|
+
if kind == "lookup":
|
|
318
|
+
reference = reference_of(f)
|
|
319
|
+
names = [str(n) for n in (reference.get("lookupNames") or [])]
|
|
320
|
+
aliases = by_aliases(names)
|
|
321
|
+
first = names[0] if names else "id"
|
|
322
|
+
if symbol:
|
|
323
|
+
if aliases:
|
|
324
|
+
return f"lookup({symbol}, '{wire}').{next(iter(aliases.values()))}('…')"
|
|
325
|
+
return f"lookup({symbol}, '{wire}').by('{first}', '…')"
|
|
326
|
+
# NO DESCRIPTOR — a Generic CRUD expansion. Its example is authored
|
|
327
|
+
# through the library by key/action/object, so the lookup has to be too,
|
|
328
|
+
# and the string form carries no typed aliases: only the generic `.by`.
|
|
329
|
+
# Emitting `lookup(<Descriptor>, …)` here was not a placeholder, it was
|
|
330
|
+
# a syntax error, on 614 pages.
|
|
331
|
+
return f"lookup('{connector_key}', '{action}', '{wire}').by('{first}', '…')"
|
|
332
|
+
type_ = str(f.get("type", "")).lower()
|
|
333
|
+
enum = f.get("enum")
|
|
334
|
+
if enum:
|
|
335
|
+
# The choice list is strings even when the field is not. Mailchimp's
|
|
336
|
+
# `email_type_option` declares `boolean` and offers "true"/"false", so
|
|
337
|
+
# emitting the choice verbatim produced `"true"` against a boolean.
|
|
338
|
+
# Where the choice cannot be typed to the declaration, fall back rather
|
|
339
|
+
# than emit something that does not compile.
|
|
340
|
+
choice = enum_value(enum[0])
|
|
341
|
+
if type_ in ("boolean", "bool"):
|
|
342
|
+
if choice.lower() in ("true", "false"):
|
|
343
|
+
return choice.lower()
|
|
344
|
+
elif type_ in ("number", "integer", "float", "double", "long"):
|
|
345
|
+
try:
|
|
346
|
+
float(choice)
|
|
347
|
+
except ValueError:
|
|
348
|
+
pass
|
|
349
|
+
else:
|
|
350
|
+
return choice
|
|
351
|
+
else:
|
|
352
|
+
return json.dumps(choice)
|
|
353
|
+
return f"input('{leaf_name(name)}')"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def leaf_name(field_name: str) -> str:
|
|
357
|
+
"""`fields.project.key` -> `projectKey`; a readable input name, not a path."""
|
|
358
|
+
parts = [p for p in field_name.replace("[*]", "").split(".") if p]
|
|
359
|
+
if not parts:
|
|
360
|
+
return "value"
|
|
361
|
+
if len(parts) == 1:
|
|
362
|
+
return parts[0]
|
|
363
|
+
tail = parts[-2:]
|
|
364
|
+
head, *rest = tail
|
|
365
|
+
return head + "".join(w[:1].upper() + w[1:] for w in rest)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class _Node:
|
|
369
|
+
"""One level of the example's input literal.
|
|
370
|
+
|
|
371
|
+
A segment can be BOTH an array and an object — `contacts[*].name` means a
|
|
372
|
+
list of objects — so array-ness is a flag on the node rather than a
|
|
373
|
+
different kind of node. Modelling it as "a list holding a value" is what an
|
|
374
|
+
earlier version did, and it crashed the moment a second field descended into
|
|
375
|
+
the same array.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
__slots__ = ("children", "is_array", "leaf")
|
|
379
|
+
|
|
380
|
+
def __init__(self) -> None:
|
|
381
|
+
self.children: dict[str, _Node] = {}
|
|
382
|
+
self.is_array = False
|
|
383
|
+
self.leaf: str | None = None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def nest_inputs(pairs: list[tuple[str, str]]) -> list[str]:
|
|
387
|
+
"""Render `{name: value}` pairs as the nested object literal an author writes.
|
|
388
|
+
|
|
389
|
+
`buildConnectorInputs` accepts either the flat dotted key or the nested
|
|
390
|
+
object, but the descriptor's generated `Inputs` type is nested — so an
|
|
391
|
+
example using flat keys would not typecheck against the very descriptor it
|
|
392
|
+
imports. Wildcards (`users[*]`) become arrays for the same reason.
|
|
393
|
+
"""
|
|
394
|
+
root = _Node()
|
|
395
|
+
for name, value in pairs:
|
|
396
|
+
node = root
|
|
397
|
+
for segment in [p for p in str(name).split(".") if p]:
|
|
398
|
+
is_array = segment.endswith("[*]")
|
|
399
|
+
key = segment[:-3] if is_array else segment
|
|
400
|
+
child = node.children.get(key)
|
|
401
|
+
if child is None:
|
|
402
|
+
child = node.children[key] = _Node()
|
|
403
|
+
child.is_array = child.is_array or is_array
|
|
404
|
+
node = child
|
|
405
|
+
node.leaf = value
|
|
406
|
+
|
|
407
|
+
def render(node: _Node, indent: int) -> list[str]:
|
|
408
|
+
pad = " " * indent
|
|
409
|
+
lines: list[str] = []
|
|
410
|
+
for key, child in node.children.items():
|
|
411
|
+
safe = key if _IDENT.fullmatch(key) else json.dumps(key)
|
|
412
|
+
if child.children:
|
|
413
|
+
open_brace, close_brace = ("[{", "}],") if child.is_array else ("{", "},")
|
|
414
|
+
lines.append(f"{pad}{safe}: {open_brace}")
|
|
415
|
+
lines += render(child, indent + 1)
|
|
416
|
+
lines.append(f"{pad}{close_brace}")
|
|
417
|
+
else:
|
|
418
|
+
value = child.leaf if child.leaf is not None else "undefined"
|
|
419
|
+
lines.append(f"{pad}{safe}: [{value}]," if child.is_array
|
|
420
|
+
else f"{pad}{safe}: {value},")
|
|
421
|
+
return lines
|
|
422
|
+
|
|
423
|
+
return render(root, 1)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
_IDENT = re.compile(r"[A-Za-z_$][A-Za-z0-9_$]*")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def example_lines(
|
|
430
|
+
j: dict, fields: list[dict], symbol: str | None, invariant: set,
|
|
431
|
+
v1def: dict | None = None,
|
|
432
|
+
) -> list[str]:
|
|
433
|
+
"""A compilable `connector(...)` call for this operation.
|
|
434
|
+
|
|
435
|
+
A page describes an operation's fields and never showed a call, so the agent
|
|
436
|
+
had to reconstitute the shape from a different document describing a general
|
|
437
|
+
form. The assembled answer is generatable from the same JSON, and the import
|
|
438
|
+
path is DERIVED rather than assumed: an operation whose schema arrives at
|
|
439
|
+
design time comes from `./connectors-local/` after a `prepare`, and saying
|
|
440
|
+
`./connectors/` for it would send the reader to a file that does not exist.
|
|
441
|
+
|
|
442
|
+
Required inputs only. The optional set runs to hundreds of fields on real
|
|
443
|
+
operations; an example showing them would not be an example.
|
|
444
|
+
"""
|
|
445
|
+
connector_key = (j.get("connector") or {}).get("key") or "<connector>"
|
|
446
|
+
operation = j.get("operation") or {}
|
|
447
|
+
runtime = j.get("runtime") or {}
|
|
448
|
+
node_type = j.get("nodeType") or ""
|
|
449
|
+
action = node_type.rsplit(".", 1)[-1]
|
|
450
|
+
# WHICH REQUIRED SET. When a descriptor exists the example is type-checked
|
|
451
|
+
# against its `Inputs` interface, so the required set must be the one that
|
|
452
|
+
# interface declares — this module derives a different vocabulary, and the
|
|
453
|
+
# two disagree about required-ness on 4,529 of 7,036 operations (about the
|
|
454
|
+
# full field set on 6,164). Brevo `get-contact-by-email` is the shape of it:
|
|
455
|
+
# the page says "Required inputs: (none)" for an operation whose descriptor
|
|
456
|
+
# requires `email`, because the path parameter is picked up by one
|
|
457
|
+
# derivation and not the other.
|
|
458
|
+
#
|
|
459
|
+
# That disagreement is REAL and PRE-EXISTING, and reconciling it rewrites
|
|
460
|
+
# most of the markdown library — its own change, with its own review. Here
|
|
461
|
+
# the example simply follows the type it is checked against, so it compiles
|
|
462
|
+
# and is honest about what the descriptor wants.
|
|
463
|
+
#
|
|
464
|
+
# The string form takes `Record<string, unknown>`, so a generic example
|
|
465
|
+
# cannot fail on required-ness and keeps this module's vocabulary.
|
|
466
|
+
if symbol:
|
|
467
|
+
# v1def ships as either a dict or a one-element list; the descriptor
|
|
468
|
+
# generator normalises it the same way.
|
|
469
|
+
raw = v1def[0] if isinstance(v1def, list) and v1def else (v1def or {})
|
|
470
|
+
# Marked with the SAME call the descriptor generator uses, so the
|
|
471
|
+
# example puts a helper on exactly the fields that have one.
|
|
472
|
+
example_fields = descriptor_mark_lookups(
|
|
473
|
+
j, descriptor_input_fields(j, raw), invariant,
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
example_fields = fields
|
|
477
|
+
required = [f for f in example_fields if f.get("required")]
|
|
478
|
+
|
|
479
|
+
schema_dynamic = bool(
|
|
480
|
+
operation.get("objectFromDiscovery")
|
|
481
|
+
or (operation.get("schemaAction") or {}).get("parents")
|
|
482
|
+
or runtime.get("requiresConnectionForSchema")
|
|
483
|
+
)
|
|
484
|
+
step = pascal_step(action)
|
|
485
|
+
|
|
486
|
+
if node_type.startswith(("uipath.connector.event.", "uipath.connector.trigger.")):
|
|
487
|
+
return event_example_lines(j, symbol, connector_key, action, step, v1def)
|
|
488
|
+
|
|
489
|
+
out = ["## Example", ""]
|
|
490
|
+
pairs = [(authoring_field(f)[0] if not authoring_field(f)[2] else f.get("name"),
|
|
491
|
+
example_value(f, symbol, invariant, connector_key, action))
|
|
492
|
+
for f in required]
|
|
493
|
+
# Import exactly what the rendered body uses. An example that references a
|
|
494
|
+
# helper it did not import does not compile, and a generated example that
|
|
495
|
+
# does not compile is worse than none: it is copied verbatim, and the reader
|
|
496
|
+
# spends turns working around a defect they did not author.
|
|
497
|
+
body_uses = {"connector"}
|
|
498
|
+
if any("lookup(" in v for _, v in pairs):
|
|
499
|
+
body_uses.add("lookup")
|
|
500
|
+
if any("input(" in v for _, v in pairs):
|
|
501
|
+
body_uses.add("input")
|
|
502
|
+
imported = ", ".join(sorted(body_uses))
|
|
503
|
+
|
|
504
|
+
out.append("```ts")
|
|
505
|
+
# THE PACKAGE, not a relative path. `@uipath/maestro-builder-sdk` is what an installed
|
|
506
|
+
# workspace resolves and what every shipped example uses; './sdk/flow-sdk.js'
|
|
507
|
+
# is a path that exists in no layout the docs describe, and an eval run
|
|
508
|
+
# caught an agent running `find / -iname flow-sdk.js` looking for it.
|
|
509
|
+
out.append(f"import {{ {imported} }} from '@uipath/maestro-builder-sdk';")
|
|
510
|
+
if symbol:
|
|
511
|
+
directory = "connectors-local" if schema_dynamic else "connectors"
|
|
512
|
+
out.append(f"import {{ {symbol} }} from './{directory}/{connector_key}.ts';")
|
|
513
|
+
out.append("")
|
|
514
|
+
head = f".step('{step}', connector({symbol}, {{"
|
|
515
|
+
else:
|
|
516
|
+
out.append("")
|
|
517
|
+
head = f".step('{step}', connector('{connector_key}', '{action}', {{"
|
|
518
|
+
|
|
519
|
+
out.append(head)
|
|
520
|
+
if pairs:
|
|
521
|
+
out += nest_inputs(pairs)
|
|
522
|
+
opts = []
|
|
523
|
+
if runtime.get("requiresConnection"):
|
|
524
|
+
opts.append(f"connection: '{connector_key.split('-')[-1]}'")
|
|
525
|
+
if runtime.get("requiresFolderKey"):
|
|
526
|
+
opts.append("folder: 'shared'")
|
|
527
|
+
if not symbol and operation.get("activityType") in ("Generic", "GenericExpanded"):
|
|
528
|
+
opts.append(f"object: '{operation.get('objectName') or '<object>'}'")
|
|
529
|
+
tail = "}" + (f", {{ {', '.join(opts)} }}" if opts else "") + "))"
|
|
530
|
+
out.append(tail)
|
|
531
|
+
out.append("```")
|
|
532
|
+
out.append("")
|
|
533
|
+
if schema_dynamic and symbol:
|
|
534
|
+
# ONE ordering per page. This trailer used to say "run prepare FIRST;
|
|
535
|
+
# the import does not resolve until you do" — which read as an
|
|
536
|
+
# instruction to prepare before authoring, on the same page whose
|
|
537
|
+
# header says author first. Two orderings on one page, and the agent
|
|
538
|
+
# takes the one it read last (observed: an agent front-loaded prepare
|
|
539
|
+
# for exactly this sentence). The import fact stays; the ordering
|
|
540
|
+
# instruction becomes the author-first contract.
|
|
541
|
+
if operation.get("objectFromDiscovery"):
|
|
542
|
+
out.append(
|
|
543
|
+
f"The descriptor comes from `./connectors-local/` and this "
|
|
544
|
+
f"import resolves only after "
|
|
545
|
+
f"`npx flow-sdk registry prepare {connector_key} {action} "
|
|
546
|
+
f"--object <name>` runs. That never blocks authoring: write "
|
|
547
|
+
f"the step first with the string form and "
|
|
548
|
+
f"`{{ object: '<name-as-the-task-said-it>' }}` — `check` "
|
|
549
|
+
f"names this exact prepare — then switch to this import."
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
out.append(
|
|
553
|
+
f"The descriptor comes from `./connectors-local/` and this "
|
|
554
|
+
f"import resolves only after "
|
|
555
|
+
f"`npx flow-sdk registry prepare {connector_key} {action}` "
|
|
556
|
+
f"runs. That never blocks authoring: the fields in this "
|
|
557
|
+
f"catalog are authorable immediately, and `check` flags any "
|
|
558
|
+
f"input outside them with this same command."
|
|
559
|
+
)
|
|
560
|
+
out.append("")
|
|
561
|
+
return out
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def event_filter_lines(j: dict, v1def: dict | None) -> list[str]:
|
|
565
|
+
"""The fields a subscription may filter on — and how complete that list is.
|
|
566
|
+
|
|
567
|
+
Without this the page named the event's PARAMETERS and stopped, so the one
|
|
568
|
+
thing an author needs in order to write `filters` was the one thing the
|
|
569
|
+
page did not say — while `check` refuses a leaf outside the vocabulary,
|
|
570
|
+
because the platform silently DROPS an unmatched one and the deployed
|
|
571
|
+
trigger then fires on everything.
|
|
572
|
+
|
|
573
|
+
The completeness caveat is the other half. Where a required query/path
|
|
574
|
+
parameter selects the schema (Jira's `project` / `issuetype`), the fields
|
|
575
|
+
below are what the connector reports with no values supplied, and no
|
|
576
|
+
discovery call accepts values — so that list is a baseline the real one
|
|
577
|
+
extends, `check` warns rather than refuses there, and the page has to say
|
|
578
|
+
so instead of implying the list is exhaustive.
|
|
579
|
+
|
|
580
|
+
TWO SOURCES, matching how the platform builds each kind of list — see
|
|
581
|
+
`Library.eventFilterFields`, which this page must agree with or `check`
|
|
582
|
+
refuses a leaf the page advertised. A CURATED event filters on the fields
|
|
583
|
+
its definition marks (`order: 1` / `isCuratedEventField`), which arrive as
|
|
584
|
+
`v1def.filterFields`; a GENERIC event filters on every field the object
|
|
585
|
+
supports GET on, which arrive as the entry's `outputSchema`. Reading the
|
|
586
|
+
marked list for both is what made Jira `record-created` advertise two
|
|
587
|
+
filterable fields where the designer offers 258.
|
|
588
|
+
"""
|
|
589
|
+
node_type = j.get("nodeType", "")
|
|
590
|
+
if not node_type.startswith(("uipath.connector.event.", "uipath.connector.trigger.")):
|
|
591
|
+
return []
|
|
592
|
+
generic = (j.get("operation") or {}).get("activityType") == "GenericTrigger"
|
|
593
|
+
fields = list(((v1def or {}).get("filterFields") or {}).get("fields") or [])
|
|
594
|
+
if generic:
|
|
595
|
+
fields += (j.get("outputSchema") or {}).get("fields") or []
|
|
596
|
+
names: list[str] = []
|
|
597
|
+
for f in fields:
|
|
598
|
+
name = f.get("name")
|
|
599
|
+
if isinstance(name, str) and name and name not in names:
|
|
600
|
+
names.append(name)
|
|
601
|
+
if not names:
|
|
602
|
+
return []
|
|
603
|
+
selectors = [
|
|
604
|
+
f["name"] for f in ((v1def or {}).get("eventParameters") or {}).get("fields") or []
|
|
605
|
+
if f.get("required") and f.get("bucket") in ("query", "path")
|
|
606
|
+
]
|
|
607
|
+
out = ["## Filtering", ""]
|
|
608
|
+
out.append(
|
|
609
|
+
"Narrow which records fire the subscription with `filters`. Each leaf is "
|
|
610
|
+
"`{ field, <operator>: value }` with exactly ONE operator "
|
|
611
|
+
"(`contains`, `startsWith`, `endsWith`, `equals`, `notEquals`, "
|
|
612
|
+
"`lessThan`, `lessThanOrEqual`, `greaterThan`, `greaterThanOrEqual`):"
|
|
613
|
+
)
|
|
614
|
+
out.append("")
|
|
615
|
+
out.append("```ts")
|
|
616
|
+
out.append(f"filters: [{{ field: '{names[0]}', contains: '<value>' }}]")
|
|
617
|
+
out.append("```")
|
|
618
|
+
out.append("")
|
|
619
|
+
out.append(
|
|
620
|
+
"Field names are CASE-SENSITIVE and only these are filterable — the "
|
|
621
|
+
"platform drops a leaf it does not recognise and the trigger then fires "
|
|
622
|
+
"on records the filter should have excluded, so `check` reports one that "
|
|
623
|
+
"is not here."
|
|
624
|
+
)
|
|
625
|
+
out.append("")
|
|
626
|
+
shown = ", ".join(f"`{n}`" for n in names[:40])
|
|
627
|
+
more = f" …and {len(names) - 40} more" if len(names) > 40 else ""
|
|
628
|
+
out.append(f"{shown}{more}")
|
|
629
|
+
out.append("")
|
|
630
|
+
if selectors:
|
|
631
|
+
listed = ", ".join(f"`{n}`" for n in selectors)
|
|
632
|
+
out.append(
|
|
633
|
+
f"> **This list is a baseline, not the whole vocabulary.** {listed} "
|
|
634
|
+
f"select this event's schema, and the connector derives its filter "
|
|
635
|
+
f"and output fields from their VALUES — which no discovery call "
|
|
636
|
+
f"accepts, so the fields above are the ones reported with none "
|
|
637
|
+
f"supplied. A name outside them may still be legal, which is why "
|
|
638
|
+
f"`check` warns rather than refuses here. Confirm it against the "
|
|
639
|
+
f"trigger in the designer before deploying."
|
|
640
|
+
)
|
|
641
|
+
out.append("")
|
|
642
|
+
elif any(
|
|
643
|
+
value for name, value in
|
|
644
|
+
(((j.get("connector") or {}).get("discovery") or {}).items()) if name != "type"
|
|
645
|
+
):
|
|
646
|
+
# Distinct from the selector caveat above, and from the activity story:
|
|
647
|
+
# `resources describe` takes `-f`/`--action`, so preparing an ACTIVITY
|
|
648
|
+
# materializes its connection-scoped fields. `uip is triggers describe`
|
|
649
|
+
# takes neither, so for an event there is no call that reaches them and
|
|
650
|
+
# the list stays short however thorough the author is. Keyed on ANY
|
|
651
|
+
# discovery flag to match `Library.eventParameterFields.filtersPartial`
|
|
652
|
+
# — jira is classified `static` with `hasFieldDiscovery: true`, and it
|
|
653
|
+
# is the connector we have actually measured growing.
|
|
654
|
+
out.append(
|
|
655
|
+
"> **This list is a subset no `prepare` can complete.** This connector "
|
|
656
|
+
"reads its schema from the connection, and the trigger metadata call "
|
|
657
|
+
"takes no parent values — `uip is triggers describe` has no `-f`, the "
|
|
658
|
+
"flag that materializes an ACTIVITY's connection-scoped fields. So a "
|
|
659
|
+
"name outside the list above may be a real field of yours; `check` warns "
|
|
660
|
+
"rather than refuses, and the designer is where to confirm it."
|
|
661
|
+
)
|
|
662
|
+
out.append("")
|
|
663
|
+
return out
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def event_example_lines(
|
|
667
|
+
j: dict, symbol: str | None, connector_key: str, action: str, step: str,
|
|
668
|
+
v1def: dict | None = None,
|
|
669
|
+
) -> list[str]:
|
|
670
|
+
"""A compilable `onEvent(...)` / `waitForEvent(...)` call for a connector EVENT.
|
|
671
|
+
|
|
672
|
+
An event is not an action: its descriptor is a `triggerDescriptor()` that
|
|
673
|
+
`onEvent`/`waitForEvent` take, and `connector(...)` does not accept it. A
|
|
674
|
+
GENERIC event (`activityType: GenericTrigger`) watches ONE object of the
|
|
675
|
+
connection and its entry names none, so the example shows the `object`
|
|
676
|
+
option that names it; a curated event has its object built in, so the
|
|
677
|
+
example shows `where` with the event parameters the entry declares.
|
|
678
|
+
"""
|
|
679
|
+
operation = j.get("operation") or {}
|
|
680
|
+
generic = operation.get("activityType") == "GenericTrigger"
|
|
681
|
+
start = node_type_is_start = j.get("nodeType", "").startswith("uipath.connector.trigger.")
|
|
682
|
+
# The SIDECAR's event parameters, for the same reason the typed `where`
|
|
683
|
+
# reads them: an entry nothing has described still carries the node's raw
|
|
684
|
+
# input definition, whose names (`filter.filters[*].id`) are not even legal
|
|
685
|
+
# object keys — the example stopped parsing when it was built from those.
|
|
686
|
+
# No parameters, no `where` in the example.
|
|
687
|
+
params = [
|
|
688
|
+
f["name"] for f in ((v1def or {}).get("eventParameters") or {}).get("fields") or []
|
|
689
|
+
if isinstance(f.get("name"), str) and not f.get("hidden")
|
|
690
|
+
]
|
|
691
|
+
factory = "onEvent" if start else "waitForEvent"
|
|
692
|
+
out = ["## Example", "", "```ts"]
|
|
693
|
+
out.append(f"import {{ {factory} }} from '@uipath/maestro-builder-sdk';")
|
|
694
|
+
if symbol:
|
|
695
|
+
out.append(f"import {{ {symbol} }} from './connectors/{connector_key}.ts';")
|
|
696
|
+
out.append("")
|
|
697
|
+
opts = []
|
|
698
|
+
where_lines: list[str] = []
|
|
699
|
+
if generic:
|
|
700
|
+
opts.append("object: '<Object>'")
|
|
701
|
+
elif params:
|
|
702
|
+
# NESTED, exactly as inputs are: the generated `…Where` interface splits a
|
|
703
|
+
# dotted parameter into an object (`channelData.channel.id` becomes
|
|
704
|
+
# `channelData: ConnectorValue<ChannelDataInput>`), so a flat key — quoted
|
|
705
|
+
# or not — is not a member of the type. Unnested, Teams'
|
|
706
|
+
# `channelData.channel.id` made the only two event examples in the library
|
|
707
|
+
# the only `## Example` fences in ~8,000 that did not parse.
|
|
708
|
+
where_lines = [" " + line for line in nest_inputs([(p, f"'<{p}>'") for p in params])]
|
|
709
|
+
opts.append(f"connection: '{connector_key.split('-')[-1]}'")
|
|
710
|
+
opts.append("folder: 'shared'")
|
|
711
|
+
lead = f"{factory}({symbol}, {{" if symbol else (
|
|
712
|
+
f"{factory}({{ connector: '{connector_key}', event: '{action}',"
|
|
713
|
+
)
|
|
714
|
+
if where_lines:
|
|
715
|
+
# Multi-line, like the action example, because the nested `where` is.
|
|
716
|
+
out.append(f".trigger({lead}" if start else f".step('{step}', {lead}")
|
|
717
|
+
out.append(" where: {")
|
|
718
|
+
out += where_lines
|
|
719
|
+
out.append(" },")
|
|
720
|
+
for option in opts:
|
|
721
|
+
out.append(f" {option},")
|
|
722
|
+
out.append("}))")
|
|
723
|
+
else:
|
|
724
|
+
call = f"{factory}({symbol}, {{ {', '.join(opts)} }})" if symbol else (
|
|
725
|
+
f"{factory}({{ connector: '{connector_key}', event: '{action}', {', '.join(opts)} }})"
|
|
726
|
+
)
|
|
727
|
+
out.append(f".trigger({call})" if start else f".step('{step}', {call})")
|
|
728
|
+
out.append("```")
|
|
729
|
+
out.append("")
|
|
730
|
+
if generic:
|
|
731
|
+
out.append(
|
|
732
|
+
f"A GENERIC event: it fires for ONE object of the connection, so `object` "
|
|
733
|
+
f"names it. `uip is triggers objects {connector_key} "
|
|
734
|
+
f"{operation.get('eventOperation') or operation.get('name') or '<EVENT>'} "
|
|
735
|
+
f"--connection-id <id>` lists the objects the bound connection exposes."
|
|
736
|
+
)
|
|
737
|
+
out.append("")
|
|
738
|
+
del node_type_is_start
|
|
739
|
+
return out
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def pascal_step(action: str) -> str:
|
|
743
|
+
"""`send-message-to-user` -> `sendMessageToUser`, a usable step name."""
|
|
744
|
+
words = [w for w in action.replace("_", "-").split("-") if w]
|
|
745
|
+
if not words:
|
|
746
|
+
return "step"
|
|
747
|
+
head, *rest = words
|
|
748
|
+
return head + "".join(w[:1].upper() + w[1:] for w in rest)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def schema_action_lines(
|
|
752
|
+
connector_key: str, object_name: str, verb: str,
|
|
753
|
+
action: dict, fields: list[dict],
|
|
754
|
+
) -> list[str]:
|
|
755
|
+
"""How to get an operation's real field set when values select it.
|
|
756
|
+
|
|
757
|
+
Jira `create-issue` describes to 2 fields with nothing supplied and 32 once
|
|
758
|
+
a project and an issue type are given; a different issue type in the same
|
|
759
|
+
project gives a different set again. Salesforce SOQL is the same mechanism
|
|
760
|
+
driven by the query text. Without this section the page reads as an
|
|
761
|
+
operation that takes two inputs.
|
|
762
|
+
"""
|
|
763
|
+
parents = action.get("parents") or []
|
|
764
|
+
if not parents:
|
|
765
|
+
return []
|
|
766
|
+
by_name = {f.get("name"): f for f in fields}
|
|
767
|
+
trigger = action.get("trigger")
|
|
768
|
+
replaces = action.get("replaces")
|
|
769
|
+
obj = object_name or "<object>"
|
|
770
|
+
|
|
771
|
+
out = ["## Resolving the field set", ""]
|
|
772
|
+
out.append(
|
|
773
|
+
"The inputs listed below are only the ones that SELECT the field set. "
|
|
774
|
+
"The real set depends on the values of "
|
|
775
|
+
+ ", ".join(f"`{p}`" for p in parents)
|
|
776
|
+
+ " — a different value gives a different set, so there is no single "
|
|
777
|
+
"field list for this operation."
|
|
778
|
+
)
|
|
779
|
+
out.append("")
|
|
780
|
+
if trigger:
|
|
781
|
+
out.append(
|
|
782
|
+
f"It is not automatic: `{trigger}` is a trigger rather than data, "
|
|
783
|
+
"so the schema has to be requested once the values are in place."
|
|
784
|
+
)
|
|
785
|
+
out.append("")
|
|
786
|
+
out.append("Resolve the parents in this order, then describe again:")
|
|
787
|
+
out.append("")
|
|
788
|
+
|
|
789
|
+
for position, parent in enumerate(parents, start=1):
|
|
790
|
+
reference = (by_name.get(parent) or {}).get("reference") or {}
|
|
791
|
+
collection = reference_collection(by_name.get(parent) or {})
|
|
792
|
+
if not collection:
|
|
793
|
+
out.append(
|
|
794
|
+
f"{position}. `{parent}` — supply the value; it is not chosen "
|
|
795
|
+
"from a list."
|
|
796
|
+
)
|
|
797
|
+
out.append("")
|
|
798
|
+
continue
|
|
799
|
+
query = reference_query(by_name.get(parent) or {})
|
|
800
|
+
pattern = reference.get("filterPattern") or ""
|
|
801
|
+
names = reference.get("lookupNames") or []
|
|
802
|
+
send = reference.get("lookupValue") or "id"
|
|
803
|
+
out.append(
|
|
804
|
+
f"{position}. `{parent}` — from `{collection}`"
|
|
805
|
+
+ (f", match on {' or '.join(f'`{n}`' for n in names)}" if names
|
|
806
|
+
else "")
|
|
807
|
+
+ f", send `{send}`."
|
|
808
|
+
)
|
|
809
|
+
# A placeholder means an earlier parent's value goes here.
|
|
810
|
+
if "{" in collection:
|
|
811
|
+
out.append(
|
|
812
|
+
" Substitute the value resolved above for the "
|
|
813
|
+
"`{…}` part of the collection."
|
|
814
|
+
)
|
|
815
|
+
out.append("")
|
|
816
|
+
out.append(" ```bash")
|
|
817
|
+
command = (
|
|
818
|
+
f" uip is resources run list {connector_key} {collection} "
|
|
819
|
+
f"--connection-id <id> \\"
|
|
820
|
+
)
|
|
821
|
+
out.append(command)
|
|
822
|
+
if pattern:
|
|
823
|
+
# The connector declares its own search. Substituting the
|
|
824
|
+
# wanted term narrows server-side and usually removes the
|
|
825
|
+
# paging: Jira project goes from 420+ rows across pages to 2
|
|
826
|
+
# in one call. It only works against the path the reference
|
|
827
|
+
# declares (`project/search`), not the plain object.
|
|
828
|
+
out.append(
|
|
829
|
+
' --query "'
|
|
830
|
+
+ pattern.replace("{filter}", "<search-term>")
|
|
831
|
+
+ '" \\'
|
|
832
|
+
)
|
|
833
|
+
elif query:
|
|
834
|
+
out.append(f' --query "{query}" \\')
|
|
835
|
+
projection = ",".join(
|
|
836
|
+
f"{n.split('[')[0]}:{n}" for n in (names[:2] or ["name"])
|
|
837
|
+
)
|
|
838
|
+
out.append(
|
|
839
|
+
f' --output-filter "{{items: items[*].{{{projection},'
|
|
840
|
+
f"send:{send}}}, page: Pagination.{{h:HasMore,t:NextPageToken}}}}\" \\"
|
|
841
|
+
)
|
|
842
|
+
out.append(" --output json")
|
|
843
|
+
out.append(" ```")
|
|
844
|
+
out.append("")
|
|
845
|
+
if pattern:
|
|
846
|
+
out.append(
|
|
847
|
+
" The connector declares its own search, so narrow with "
|
|
848
|
+
"the term the user gave rather than reading pages: one "
|
|
849
|
+
"call usually returns a handful, and `page` comes back "
|
|
850
|
+
"either absent or with `h` as `\"false\"`. Page only if "
|
|
851
|
+
"the term matches nothing."
|
|
852
|
+
)
|
|
853
|
+
else:
|
|
854
|
+
out.append(
|
|
855
|
+
" Page it: `page.h` is the `elements-has-more` response "
|
|
856
|
+
"header verbatim, so it is the string `\"true\"` rather "
|
|
857
|
+
"than a boolean, and `page` is absent altogether when the "
|
|
858
|
+
"connector sends no pagination headers. While it reads "
|
|
859
|
+
"`\"true\"` and the value has not appeared, pass `page.t` "
|
|
860
|
+
"back as `--query \"nextPage=<token>\"` and read the next "
|
|
861
|
+
"page. Stop on the match. The projection keeps each page "
|
|
862
|
+
"small but does not reduce the call count."
|
|
863
|
+
)
|
|
864
|
+
out.append("")
|
|
865
|
+
out.append(
|
|
866
|
+
" Before paging, check this field's `Reference` in the "
|
|
867
|
+
"`describe` output for a `FilterPattern`: it is "
|
|
868
|
+
"connection-scoped, so it is not in this catalog, but when "
|
|
869
|
+
"present it is the connector's own search and removes the "
|
|
870
|
+
"paging — Jira's project lookup declares `query={filter}`, "
|
|
871
|
+
"which turns 420-plus rows across pages into two in one call. "
|
|
872
|
+
"Substitute the wanted term into whatever pattern this field "
|
|
873
|
+
"declares; the shape is:"
|
|
874
|
+
)
|
|
875
|
+
out.append("")
|
|
876
|
+
out.append(" ```bash")
|
|
877
|
+
out.append(
|
|
878
|
+
f" uip is resources run list {connector_key} {collection} "
|
|
879
|
+
f"--connection-id <id> \\"
|
|
880
|
+
)
|
|
881
|
+
out.append(
|
|
882
|
+
' --query "<filter-pattern with the term substituted>" '
|
|
883
|
+
"--output json"
|
|
884
|
+
)
|
|
885
|
+
out.append(" ```")
|
|
886
|
+
out.append("")
|
|
887
|
+
|
|
888
|
+
supplied = " ".join(f"-f {p}=<value>" for p in parents)
|
|
889
|
+
out.append(f"{len(parents) + 1}. Describe again with the resolved values:")
|
|
890
|
+
out.append("")
|
|
891
|
+
out.append(" ```bash")
|
|
892
|
+
out.append(
|
|
893
|
+
f" uip is resources describe {connector_key} {obj} "
|
|
894
|
+
f"--operation {verb} \\"
|
|
895
|
+
)
|
|
896
|
+
out.append(f" --connection-id <id> {supplied} --output json")
|
|
897
|
+
out.append(" ```")
|
|
898
|
+
out.append("")
|
|
899
|
+
if replaces == "output":
|
|
900
|
+
out.append(
|
|
901
|
+
" This replaces the **response** fields — it tells you the shape "
|
|
902
|
+
"of what comes back, not what to send."
|
|
903
|
+
)
|
|
904
|
+
elif replaces == "both":
|
|
905
|
+
out.append(
|
|
906
|
+
" This replaces the **request** fields AND the **response** "
|
|
907
|
+
"fields — both halves of this operation change with the values "
|
|
908
|
+
"above, so read the response shape from the same call."
|
|
909
|
+
)
|
|
910
|
+
else:
|
|
911
|
+
out.append(" This replaces the **request** fields.")
|
|
912
|
+
out.append("")
|
|
913
|
+
# Only meaningful where the action rewrites the INPUT. Where it rewrites
|
|
914
|
+
# the output — a SOQL query, Jira `get-issue` — a resolved call leaves the
|
|
915
|
+
# request fields as exactly the parents, so telling a reader to treat that
|
|
916
|
+
# as failure makes every success look broken. Matches the gate in the CLI
|
|
917
|
+
# (`isParentOnlyResult`, UiPath/cli#3773).
|
|
918
|
+
if replaces in ("input", "both"):
|
|
919
|
+
out.append(
|
|
920
|
+
"If the result contains only the parents again, one of the values "
|
|
921
|
+
"does not exist on this connection. The call reports `Success` "
|
|
922
|
+
"either way, so the count alone cannot tell that apart from having "
|
|
923
|
+
"supplied nothing — check whether anything beyond the parents came "
|
|
924
|
+
"back. Too FEW parents fails loudly instead, with "
|
|
925
|
+
"`No api-type ObjectAction matched`."
|
|
926
|
+
)
|
|
927
|
+
out.append("")
|
|
928
|
+
return out
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def field_line(f: dict) -> str:
|
|
932
|
+
name, type_, wire_name = authoring_field(f)
|
|
933
|
+
desc = (f.get("description") or "").replace("\n", " ").strip()
|
|
934
|
+
enum = f.get("enum")
|
|
935
|
+
suffix = f", enum: {' | '.join(enum_value(e) for e in enum)}" if enum else ""
|
|
936
|
+
if wire_name:
|
|
937
|
+
suffix += f", wire field: `{wire_name}`"
|
|
938
|
+
# The wire slot, for a QUERY, PATH, or MULTIPART parameter. Body fields stay
|
|
939
|
+
# untagged so every existing line is byte-identical: `location` is the new
|
|
940
|
+
# information, and a field with none has always been a body field.
|
|
941
|
+
loc = f.get("location")
|
|
942
|
+
if loc in ("query", "path", "multipart"):
|
|
943
|
+
suffix += f", {loc}"
|
|
944
|
+
# Name the collection so the line itself says "this is a lookup, not free
|
|
945
|
+
# text". `Resolving lookups` below carries the call that resolves it.
|
|
946
|
+
collection = reference_collection(f)
|
|
947
|
+
if collection:
|
|
948
|
+
suffix += f", lookup: {collection}"
|
|
949
|
+
head = f"- `{name}` ({type_}{suffix})"
|
|
950
|
+
return f"{head} — {desc}" if desc else head
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
# `operation.parameters[].type` → the wire slot, mirroring
|
|
954
|
+
# `typescript/sdk/src/core/library.ts`'s PARAM_LOCATIONS.
|
|
955
|
+
PARAM_LOCATIONS = {
|
|
956
|
+
"query": "query", "path": "path", "multipart": "multipart"
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
# A parameter's `dataType` → the scalar names `compile`'s literal type-check
|
|
960
|
+
# understands (library.ts's `scalarType`).
|
|
961
|
+
_SCALARS = {
|
|
962
|
+
"integer": "number", "number": "number", "double": "number",
|
|
963
|
+
"float": "number", "long": "number",
|
|
964
|
+
"boolean": "boolean", "string": "string",
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def scalar_type(data_type: object) -> str:
|
|
969
|
+
return _SCALARS.get(data_type, data_type if isinstance(data_type, str) else "?")
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def param_fields(j: dict) -> list[dict]:
|
|
973
|
+
"""The op's QUERY, PATH, and MULTIPART parameters as body-shaped fields.
|
|
974
|
+
|
|
975
|
+
These are inputs the COMPILER accepts — `core/library.ts`'s `readFields`
|
|
976
|
+
unions them with the v1def's body fields and tags each with its `location`,
|
|
977
|
+
which is what `serialize` routes on (flow-builder-sdk#132). The markdown was
|
|
978
|
+
written before that, so it documented the body half only.
|
|
979
|
+
|
|
980
|
+
That gap was large and, worse, silently wrong about REQUIREDNESS. Measured on
|
|
981
|
+
the shipped library (7,030 entries, 2026-07-31):
|
|
982
|
+
|
|
983
|
+
| | count |
|
|
984
|
+
|---|--:|
|
|
985
|
+
| entries carrying `operation.parameters` | **5,687** (81%) |
|
|
986
|
+
| parameters dropped | **14,872** (10,710 query · 4,162 path) |
|
|
987
|
+
| entries with a REQUIRED parameter | **4,413** (5,563 required params) |
|
|
988
|
+
|
|
989
|
+
Every one of those 4,413 rendered *"Required inputs: _(none)_"* — including
|
|
990
|
+
Jira `get-issue`, whose `{issueId}` path parameter the operation cannot run
|
|
991
|
+
without, and Data Service `query-entity-records`, whose required `entityName`
|
|
992
|
+
picks the entity. The tasks that used those ops named the parameters in their
|
|
993
|
+
own prompts, which is why nothing surfaced it.
|
|
994
|
+
"""
|
|
995
|
+
out = []
|
|
996
|
+
for p in (j.get("operation") or {}).get("parameters") or []:
|
|
997
|
+
name = p.get("name")
|
|
998
|
+
loc = PARAM_LOCATIONS.get(p.get("type"))
|
|
999
|
+
if not isinstance(name, str) or not name or not loc:
|
|
1000
|
+
continue
|
|
1001
|
+
# A string-typed multipart part is the transport container the runtime
|
|
1002
|
+
# composes from the body fields — documenting it as the REQUIRED input
|
|
1003
|
+
# beside the real `body.content` is exactly what sent authors down
|
|
1004
|
+
# `body: 'text'` (value ignored at dispatch) and `'body.content'`
|
|
1005
|
+
# (refused by compile) on 2026-09-01. Only file parts are inputs.
|
|
1006
|
+
if is_container_part(p):
|
|
1007
|
+
continue
|
|
1008
|
+
f = {"name": name, "type": scalar_type(p.get("dataType")),
|
|
1009
|
+
"required": p.get("required") is True, "location": loc}
|
|
1010
|
+
if p.get("description"):
|
|
1011
|
+
f["description"] = p["description"]
|
|
1012
|
+
if p.get("displayName"):
|
|
1013
|
+
f["displayName"] = p["displayName"]
|
|
1014
|
+
# `reference` says the value is an id resolved from a collection, not
|
|
1015
|
+
# free text. Without it the page reads as "type anything here", which
|
|
1016
|
+
# is how an author ends up sending a channel NAME where the API wants
|
|
1017
|
+
# a channel id.
|
|
1018
|
+
if p.get("reference"):
|
|
1019
|
+
f["reference"] = p["reference"]
|
|
1020
|
+
# A parameter's `enum` is deprecated in favour of `enhancedEnum`; the
|
|
1021
|
+
# latter is what connectors actually populate.
|
|
1022
|
+
if p.get("enum"):
|
|
1023
|
+
f["enum"] = p["enum"]
|
|
1024
|
+
elif p.get("enhancedEnum"):
|
|
1025
|
+
f["enum"] = p["enhancedEnum"]
|
|
1026
|
+
out.append(f)
|
|
1027
|
+
return out
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def catalog_multipart_fields(j: dict) -> list[dict]:
|
|
1031
|
+
"""File fields whose older catalog entry omitted the multipart parameter."""
|
|
1032
|
+
out = []
|
|
1033
|
+
for field in (j.get("inputSchema") or {}).get("fields") or []:
|
|
1034
|
+
if (
|
|
1035
|
+
not field.get("name")
|
|
1036
|
+
or str(field.get("type") or "").lower() != "file"
|
|
1037
|
+
):
|
|
1038
|
+
continue
|
|
1039
|
+
multipart = dict(field)
|
|
1040
|
+
multipart["location"] = "multipart"
|
|
1041
|
+
out.append(multipart)
|
|
1042
|
+
return out
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def input_fields(j: dict, v1def: dict | None) -> list[dict]:
|
|
1046
|
+
"""The op's input fields, taken from the set the COMPILER validates against.
|
|
1047
|
+
|
|
1048
|
+
There are two candidate schemas per op and they disagree for 47% of curated
|
|
1049
|
+
ops, so this choice is the whole point of the function:
|
|
1050
|
+
|
|
1051
|
+
* ``<op>.v1def.json`` → ``inputDefinition.fields`` — the Flow node's input
|
|
1052
|
+
contract. ``core/library.ts:106`` loads exactly this into ``fields``, and
|
|
1053
|
+
``serialize.ts`` rejects anything outside it with `unknown input "…"`. The
|
|
1054
|
+
generated typed descriptors use it too.
|
|
1055
|
+
* ``<op>.json`` → ``inputSchema.fields`` — the connector's *API-level* schema
|
|
1056
|
+
from the registry. Richer, and it includes parameters the Flow node does not
|
|
1057
|
+
expose (pagination, ``where``/``orderBy``, upload ``file`` …).
|
|
1058
|
+
|
|
1059
|
+
File fields are the one exception: the Flow artifact has a multipart slot,
|
|
1060
|
+
and older catalog snapshots can carry the file only in ``inputSchema``.
|
|
1061
|
+
``catalog_multipart_fields`` adds that transport field explicitly.
|
|
1062
|
+
|
|
1063
|
+
Documenting the second set makes the catalog advertise inputs the compiler
|
|
1064
|
+
refuses. Measured over the shipped library: of 1,337 curated ops, **585**
|
|
1065
|
+
listed at least one field that fails to compile, e.g. `pageSize` on
|
|
1066
|
+
`uipath-act-act365.list-all-activities` →
|
|
1067
|
+
`unknown input "pageSize"`. Another **49** hid a usable field the markdown
|
|
1068
|
+
never mentioned. Where the name sets already agree (703 ops) the required
|
|
1069
|
+
flags agree too, so those files are unchanged.
|
|
1070
|
+
|
|
1071
|
+
Falls back to ``inputSchema`` when an op ships no v1def, so nothing is lost
|
|
1072
|
+
for entries the Flow layer does not model.
|
|
1073
|
+
"""
|
|
1074
|
+
fields = ((v1def or {}).get("inputDefinition") or {}).get("fields")
|
|
1075
|
+
if fields is None:
|
|
1076
|
+
fields = (j.get("inputSchema") or {}).get("fields") or []
|
|
1077
|
+
return fields
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def render_op(j: dict, v1def: dict | None = None, symbol: str | None = None,
|
|
1081
|
+
invariant: set | None = None) -> str:
|
|
1082
|
+
"""Render one library entry to markdown.
|
|
1083
|
+
|
|
1084
|
+
Header layout (verb-first; both Curated and Generic share the same
|
|
1085
|
+
shape — only the values differ):
|
|
1086
|
+
# <nodeType>@<version>
|
|
1087
|
+
**Connector:** <key> (<name>)
|
|
1088
|
+
**Operation:** <verb> ← `op.name`, e.g. Create / Delete / Retrieve
|
|
1089
|
+
**Object:** <objectName> ← when present
|
|
1090
|
+
**Label:** <human-readable> ← `display.label`, when distinct
|
|
1091
|
+
**Description:** ...
|
|
1092
|
+
**HTTP:** <method> <pathTemplate-or-path>
|
|
1093
|
+
"""
|
|
1094
|
+
invariant = invariant or set()
|
|
1095
|
+
node_type = j.get("nodeType", "?")
|
|
1096
|
+
version = j.get("version", "?")
|
|
1097
|
+
conn = j.get("connector", {}) or {}
|
|
1098
|
+
op = j.get("operation", {}) or {}
|
|
1099
|
+
display = j.get("display", {}) or {}
|
|
1100
|
+
|
|
1101
|
+
out: list[str] = []
|
|
1102
|
+
out.append(f"# {node_type}@{version}")
|
|
1103
|
+
out.append("")
|
|
1104
|
+
|
|
1105
|
+
conn_label = conn.get("key", "?")
|
|
1106
|
+
if conn.get("name"):
|
|
1107
|
+
conn_label += f" ({conn['name']})"
|
|
1108
|
+
out.append(f"**Connector:** {conn_label}")
|
|
1109
|
+
|
|
1110
|
+
verb = op.get("name") or ""
|
|
1111
|
+
if verb:
|
|
1112
|
+
out.append(f"**Operation:** {verb}")
|
|
1113
|
+
|
|
1114
|
+
object_name = op.get("objectName") or ""
|
|
1115
|
+
if object_name:
|
|
1116
|
+
out.append(f"**Object:** `{object_name}`")
|
|
1117
|
+
|
|
1118
|
+
label = (display.get("label") or "").strip()
|
|
1119
|
+
# Only show the human-readable label when it adds info beyond
|
|
1120
|
+
# `<verb> <object>` (which an agent can reconstruct).
|
|
1121
|
+
if label:
|
|
1122
|
+
out.append(f"**Label:** {label}")
|
|
1123
|
+
|
|
1124
|
+
desc = (display.get("description") or "").strip()
|
|
1125
|
+
if desc:
|
|
1126
|
+
out.append(f"**Description:** {desc}")
|
|
1127
|
+
|
|
1128
|
+
http_method = op.get("httpMethod") or ""
|
|
1129
|
+
http_path = op.get("pathTemplate") or op.get("path") or ""
|
|
1130
|
+
if http_method and http_path:
|
|
1131
|
+
out.append(f"**HTTP:** {http_method} {http_path}")
|
|
1132
|
+
|
|
1133
|
+
out.append("")
|
|
1134
|
+
|
|
1135
|
+
# Phase 1 marks every activity on a connector that advertises object/field
|
|
1136
|
+
# discovery. Some activities are fully static, while others gain fields or
|
|
1137
|
+
# objects from a live connection. Point the agent at `registry prepare`
|
|
1138
|
+
# when the static library does not contain what it needs, without claiming
|
|
1139
|
+
# every marked operation is necessarily incomplete.
|
|
1140
|
+
if (j.get("runtime") or {}).get("requiresConnectionForSchema"):
|
|
1141
|
+
key = conn.get("key") or "<key>"
|
|
1142
|
+
prefix = f"uipath.connector.{key}."
|
|
1143
|
+
action = node_type[len(prefix):] if node_type.startswith(prefix) else node_type.rsplit(".", 1)[-1]
|
|
1144
|
+
# A discovery entry is missing something different from the rest, and
|
|
1145
|
+
# `-f` cannot supply it. Of the 1,635 tier-1 pages carrying this block,
|
|
1146
|
+
# 30 are discovery entries: they have no inputs, and what has to be
|
|
1147
|
+
# resolved is the OBJECT. Pointing them at the parent-field loop and a
|
|
1148
|
+
# `-f <parent-field>=<value>` command describes a job that does not
|
|
1149
|
+
# apply, with a flag that cannot do it — `registry prepare` takes
|
|
1150
|
+
# `--object` for this.
|
|
1151
|
+
parents = ((j.get("operation") or {}).get("schemaAction") or {}).get(
|
|
1152
|
+
"parents"
|
|
1153
|
+
) or []
|
|
1154
|
+
discovery = bool((j.get("operation") or {}).get("objectFromDiscovery"))
|
|
1155
|
+
if discovery:
|
|
1156
|
+
out.append(
|
|
1157
|
+
"> ⚠️ **The object is not in this catalog.** This node works on "
|
|
1158
|
+
"any object in the connection, including custom ones, so it "
|
|
1159
|
+
"carries no inputs here — what has to be resolved is the "
|
|
1160
|
+
"OBJECT, not a field value. Author first, then one command "
|
|
1161
|
+
"resolves it; **Choosing the object** below has the rules."
|
|
1162
|
+
)
|
|
1163
|
+
supply = "--object <name-as-the-task-said-it>"
|
|
1164
|
+
else:
|
|
1165
|
+
out.append(
|
|
1166
|
+
"> ⚠️ **This operation resolves its real inputs from a live "
|
|
1167
|
+
"connection.** The inputs listed below are only what the "
|
|
1168
|
+
"tenant-agnostic snapshot carries. Do NOT hand-guess the "
|
|
1169
|
+
"missing names."
|
|
1170
|
+
)
|
|
1171
|
+
out.append(">")
|
|
1172
|
+
if parents:
|
|
1173
|
+
# The section below names them, with the call that resolves
|
|
1174
|
+
# each — repeating the generic advice here just lengthens it.
|
|
1175
|
+
out.append(
|
|
1176
|
+
"> Its parents are named outright, with the call that "
|
|
1177
|
+
"resolves each one, under **Resolving the field set** "
|
|
1178
|
+
"below. Read that first."
|
|
1179
|
+
)
|
|
1180
|
+
else:
|
|
1181
|
+
# Kept even when the entry declares no inputs: they arrive from
|
|
1182
|
+
# the live describe, and the rule applies to them when they do.
|
|
1183
|
+
# `test_dynamic_schema_markdown_teaches_the_parent_field_loop`
|
|
1184
|
+
# holds this on every schema-dynamic op deliberately.
|
|
1185
|
+
out.append(
|
|
1186
|
+
"> Any input whose reference path carries a `{placeholder}` "
|
|
1187
|
+
"is a parent: resolve a value for EVERY parent first, then "
|
|
1188
|
+
"prepare the operation with all of them. Too few parents is "
|
|
1189
|
+
"refused; a wrong VALUE is not — it resolves right back to "
|
|
1190
|
+
"the parent fields."
|
|
1191
|
+
)
|
|
1192
|
+
# A bare prepare must never read as sufficient here: an op with no
|
|
1193
|
+
# declared inputs can still turn out to have parents once the live
|
|
1194
|
+
# schema arrives, and "2 fields came back" looks identical to
|
|
1195
|
+
# success. So the `-f` stays visible even when the catalog cannot
|
|
1196
|
+
# name the parents — only the prose above adapts.
|
|
1197
|
+
supply = (
|
|
1198
|
+
" ".join(f"-f {name}=<value>" for name in parents)
|
|
1199
|
+
if parents
|
|
1200
|
+
else "-f <parent-field>=<value> ..."
|
|
1201
|
+
)
|
|
1202
|
+
out.append(">")
|
|
1203
|
+
out.append("> ```bash")
|
|
1204
|
+
# ONE spelling on every page, and it is the one `check` prints. #660
|
|
1205
|
+
# measured the cost of the alternative: agents follow the documented
|
|
1206
|
+
# form, so a legacy `prepare-connector --connection-id <id>` here while
|
|
1207
|
+
# CUSTOM_FIELDS_UNPREPARED says `npx flow-sdk registry prepare` is two
|
|
1208
|
+
# routes on one page — and the agent takes the one it already knows.
|
|
1209
|
+
# No `--connection-id`: prepare discovers the connection itself and
|
|
1210
|
+
# writes bindings.json (folder key included).
|
|
1211
|
+
if discovery:
|
|
1212
|
+
# The name is matched, not equality-tested, so the task's casing
|
|
1213
|
+
# is fine as given.
|
|
1214
|
+
out.append(
|
|
1215
|
+
f"> npx flow-sdk registry prepare {key} {action} {supply}"
|
|
1216
|
+
)
|
|
1217
|
+
elif supply:
|
|
1218
|
+
out.append(f"> npx flow-sdk registry prepare {key} {action} \\")
|
|
1219
|
+
out.append(f"> {supply}")
|
|
1220
|
+
else:
|
|
1221
|
+
out.append(f"> npx flow-sdk registry prepare {key} {action}")
|
|
1222
|
+
out.append("> ```")
|
|
1223
|
+
out.append(">")
|
|
1224
|
+
overlay = (
|
|
1225
|
+
"> It writes a `./connectors-local/` overlay the compiler "
|
|
1226
|
+
"auto-detects (its fields are unioned onto this entry)."
|
|
1227
|
+
)
|
|
1228
|
+
if not discovery:
|
|
1229
|
+
# Pointing a discovery entry at the parent-field loop sends it to a
|
|
1230
|
+
# section about resolving values for parents it does not have.
|
|
1231
|
+
overlay += (
|
|
1232
|
+
" The full loop — how to find the parents and resolve their "
|
|
1233
|
+
"values — is in the connector guide "
|
|
1234
|
+
"(`references/connector-params.md`) under \"Schema-dynamic "
|
|
1235
|
+
"operations: the parent-field loop\"."
|
|
1236
|
+
)
|
|
1237
|
+
out.append(overlay)
|
|
1238
|
+
out.append(
|
|
1239
|
+
"> This requires a working live connection. For an explicitly "
|
|
1240
|
+
"offline/validate-only flow that needs no connection-specific fields, "
|
|
1241
|
+
"use the published static inputs below instead; a placeholder "
|
|
1242
|
+
"connection cannot materialize a live schema. If that input list is "
|
|
1243
|
+
"empty, pass `{}`: offline evidence proves operation identity and "
|
|
1244
|
+
"control routing, not the provider payload or dispatch."
|
|
1245
|
+
)
|
|
1246
|
+
out.append("")
|
|
1247
|
+
|
|
1248
|
+
# Body fields, catalog-only files, then declared parameters. Older snapshots
|
|
1249
|
+
# omitted a multipart parameter for some file uploads, while newer/live
|
|
1250
|
+
# entries declare it directly. A parameter remains authoritative on location,
|
|
1251
|
+
# so it REPLACES a same-named body or inferred multipart field.
|
|
1252
|
+
fields = [dict(f) for f in input_fields(j, v1def)]
|
|
1253
|
+
for p in [*catalog_multipart_fields(j), *param_fields(j)]:
|
|
1254
|
+
at = next((i for i, f in enumerate(fields) if f.get("name") == p["name"]), -1)
|
|
1255
|
+
if at >= 0:
|
|
1256
|
+
# Fill gaps rather than replace: the entry's field may already
|
|
1257
|
+
# carry a `reference` or `enum` this projection does not know
|
|
1258
|
+
# about, and overwriting it loses the lookup.
|
|
1259
|
+
merged = dict(fields[at])
|
|
1260
|
+
merged.update({k: v for k, v in p.items() if v not in (None, "")})
|
|
1261
|
+
for key in ("reference", "enum", "displayName"):
|
|
1262
|
+
if key in fields[at] and key not in p:
|
|
1263
|
+
merged[key] = fields[at][key]
|
|
1264
|
+
fields[at] = merged
|
|
1265
|
+
else:
|
|
1266
|
+
fields.append(p)
|
|
1267
|
+
required = [f for f in fields if f.get("required")]
|
|
1268
|
+
optional = [f for f in fields if not f.get("required")]
|
|
1269
|
+
|
|
1270
|
+
# Object selection comes before field selection, so it goes above the
|
|
1271
|
+
# inputs. For these operations the inputs below are whatever holds for
|
|
1272
|
+
# every object; the object's own fields arrive from step 2.
|
|
1273
|
+
if op.get("objectFromDiscovery"):
|
|
1274
|
+
connector_key = conn.get("key") or "<connector>"
|
|
1275
|
+
verb = op.get("name") or "<operation>"
|
|
1276
|
+
prefix = f"uipath.connector.{connector_key}."
|
|
1277
|
+
md_action = (
|
|
1278
|
+
node_type[len(prefix):] if node_type.startswith(prefix)
|
|
1279
|
+
else node_type.rsplit(".", 1)[-1]
|
|
1280
|
+
)
|
|
1281
|
+
out.append("## Choosing the object")
|
|
1282
|
+
out.append("")
|
|
1283
|
+
out.append(
|
|
1284
|
+
"This operation works on any object in the connection, including "
|
|
1285
|
+
"custom ones, so the object list is not in this catalog. Do not "
|
|
1286
|
+
"run discovery commands and do not hand-filter the catalog — "
|
|
1287
|
+
"author the step with the object name as the task said it (any "
|
|
1288
|
+
"casing), then one command matches it deterministically:"
|
|
1289
|
+
)
|
|
1290
|
+
out.append("")
|
|
1291
|
+
out.append("```bash")
|
|
1292
|
+
out.append(
|
|
1293
|
+
f"npx flow-sdk registry prepare {connector_key} {md_action} "
|
|
1294
|
+
"--object <name-as-the-task-said-it>"
|
|
1295
|
+
)
|
|
1296
|
+
out.append("```")
|
|
1297
|
+
out.append("")
|
|
1298
|
+
out.append(
|
|
1299
|
+
"The name is matched, never equality-tested: exact, then "
|
|
1300
|
+
"case-insensitive on API name and display name (`matched "
|
|
1301
|
+
"\"Company__C\" -> Company__c` is printed), narrowed to objects "
|
|
1302
|
+
f"that support {verb}. It then fetches the object's "
|
|
1303
|
+
"connection-resolved fields, generates the typed descriptor, and "
|
|
1304
|
+
"prints the import line to use."
|
|
1305
|
+
)
|
|
1306
|
+
out.append("")
|
|
1307
|
+
out.append(
|
|
1308
|
+
"**If the name matches several objects, the command refuses and "
|
|
1309
|
+
"prints the candidates** (API name, display name, custom, "
|
|
1310
|
+
"operations). `Custom: yes` marks an object that exists only on "
|
|
1311
|
+
"this tenant, so it could never have been in this catalog — a "
|
|
1312
|
+
"task that says \"custom\" means one of those rows. Pick by the "
|
|
1313
|
+
"task's wording, or present the candidates and let the user "
|
|
1314
|
+
"choose; never pick for them, and do not assume the user's "
|
|
1315
|
+
"wording is the API name. Then re-run with the chosen name."
|
|
1316
|
+
)
|
|
1317
|
+
out.append("")
|
|
1318
|
+
out.append(
|
|
1319
|
+
"**If nothing matches**, the command has already retried against "
|
|
1320
|
+
"a refreshed catalog before saying so — the object genuinely is "
|
|
1321
|
+
"not on this connection. Check the value against the task's "
|
|
1322
|
+
"words, or surface it to the user rather than inventing one."
|
|
1323
|
+
)
|
|
1324
|
+
out.append("")
|
|
1325
|
+
|
|
1326
|
+
action = op.get("schemaAction")
|
|
1327
|
+
if action:
|
|
1328
|
+
out.extend(schema_action_lines(
|
|
1329
|
+
conn.get("key") or "<connector>",
|
|
1330
|
+
op.get("objectName") or "",
|
|
1331
|
+
op.get("name") or "<operation>",
|
|
1332
|
+
action,
|
|
1333
|
+
fields,
|
|
1334
|
+
))
|
|
1335
|
+
|
|
1336
|
+
out.append("## Required inputs")
|
|
1337
|
+
out.append("")
|
|
1338
|
+
if required:
|
|
1339
|
+
out.extend(field_line(f) for f in required)
|
|
1340
|
+
else:
|
|
1341
|
+
out.append("_(none)_")
|
|
1342
|
+
out.append("")
|
|
1343
|
+
|
|
1344
|
+
if optional:
|
|
1345
|
+
out.append("## Optional inputs")
|
|
1346
|
+
out.append("")
|
|
1347
|
+
out.extend(field_line(f) for f in optional)
|
|
1348
|
+
out.append("")
|
|
1349
|
+
|
|
1350
|
+
lookups = lookup_lines(
|
|
1351
|
+
conn.get("key") or "<connector>",
|
|
1352
|
+
node_type.rsplit(".", 1)[-1],
|
|
1353
|
+
fields, symbol, invariant,
|
|
1354
|
+
)
|
|
1355
|
+
if lookups:
|
|
1356
|
+
out.append("## Resolving lookups")
|
|
1357
|
+
out.append("")
|
|
1358
|
+
out.extend(lookups)
|
|
1359
|
+
out.append("")
|
|
1360
|
+
|
|
1361
|
+
# The example goes AFTER the lookups section on purpose: it uses the helpers
|
|
1362
|
+
# that section introduces, so a reader meets the rule before the usage.
|
|
1363
|
+
out.extend(example_lines(j, fields, symbol, invariant, v1def))
|
|
1364
|
+
|
|
1365
|
+
out.extend(event_filter_lines(j, v1def))
|
|
1366
|
+
|
|
1367
|
+
out_fields = (j.get("outputSchema") or {}).get("fields") or []
|
|
1368
|
+
if out_fields:
|
|
1369
|
+
out.append("## Outputs")
|
|
1370
|
+
out.append("")
|
|
1371
|
+
out.extend(field_line(f) for f in out_fields)
|
|
1372
|
+
out.append("")
|
|
1373
|
+
|
|
1374
|
+
return "\n".join(out)
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
def parse_args() -> argparse.Namespace:
|
|
1378
|
+
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
1379
|
+
p.add_argument("--source", required=True,
|
|
1380
|
+
help="Source JSON library dir (must contain index.json)")
|
|
1381
|
+
p.add_argument("--output", required=True,
|
|
1382
|
+
help="Destination dir for the markdown library")
|
|
1383
|
+
return p.parse_args()
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def main() -> int:
|
|
1387
|
+
args = parse_args()
|
|
1388
|
+
src = Path(args.source).resolve()
|
|
1389
|
+
dst = Path(args.output).resolve()
|
|
1390
|
+
|
|
1391
|
+
index_path = src / "index.json"
|
|
1392
|
+
if not index_path.is_file():
|
|
1393
|
+
print(f"source library missing index.json: {index_path}", file=sys.stderr)
|
|
1394
|
+
return 1
|
|
1395
|
+
|
|
1396
|
+
index = json.loads(index_path.read_text())
|
|
1397
|
+
entries = index.get("entries", []) or []
|
|
1398
|
+
# The SAME map the descriptor generator uses, so a symbol cited here is one
|
|
1399
|
+
# it actually emitted. An entry absent from the map has no descriptor and
|
|
1400
|
+
# its example is authored through the library instead (see symbol_index).
|
|
1401
|
+
symbols = symbol_index(entries)
|
|
1402
|
+
invariant = load_invariant_collections(src)
|
|
1403
|
+
|
|
1404
|
+
# Wipe and recreate destination so stale entries don't linger.
|
|
1405
|
+
if dst.exists():
|
|
1406
|
+
shutil.rmtree(dst)
|
|
1407
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
1408
|
+
|
|
1409
|
+
converted = 0
|
|
1410
|
+
for entry in entries:
|
|
1411
|
+
rel_json = entry.get("path") or ""
|
|
1412
|
+
if not rel_json or not rel_json.endswith(".json"):
|
|
1413
|
+
continue
|
|
1414
|
+
src_path = src / rel_json
|
|
1415
|
+
if not src_path.is_file():
|
|
1416
|
+
print(f" WARN: missing source entry {src_path}", file=sys.stderr)
|
|
1417
|
+
continue
|
|
1418
|
+
j = json.loads(src_path.read_text())
|
|
1419
|
+
# The Flow node shape lives beside the entry as `<op>.v1def.json`; its
|
|
1420
|
+
# inputDefinition is what the compiler validates, so inputs are rendered
|
|
1421
|
+
# from it (see input_fields). Outputs stay on the entry's outputSchema —
|
|
1422
|
+
# v1def carries no output fields, and the descriptor generator agrees.
|
|
1423
|
+
v1_path = src_path.with_name(src_path.name[:-5] + ".v1def.json")
|
|
1424
|
+
v1def = json.loads(v1_path.read_text()) if v1_path.is_file() else None
|
|
1425
|
+
md = render_op(j, v1def, symbols.get(entry.get("nodeType")), invariant)
|
|
1426
|
+
rel_md = rel_json[:-5] + ".md"
|
|
1427
|
+
out_path = dst / rel_md
|
|
1428
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1429
|
+
out_path.write_text(md)
|
|
1430
|
+
# Mutate the entry in place so the rewritten index points at .md.
|
|
1431
|
+
entry["path"] = rel_md
|
|
1432
|
+
converted += 1
|
|
1433
|
+
|
|
1434
|
+
(dst / "index.json").write_text(json.dumps(index, indent=2) + "\n")
|
|
1435
|
+
|
|
1436
|
+
print(f"wrote {converted} markdown ops to {dst}")
|
|
1437
|
+
return 0
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
if __name__ == "__main__":
|
|
1441
|
+
sys.exit(main())
|