@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,936 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate statically-typed TypeScript connector descriptors from the canonical
|
|
3
|
+
JSON connector library.
|
|
4
|
+
|
|
5
|
+
For every **Curated** and **HTTP-request** op, emit one `descriptor<Inputs,
|
|
6
|
+
Outputs>()` const plus its `Inputs`/`Outputs` interfaces, grouped one module per
|
|
7
|
+
connector:
|
|
8
|
+
|
|
9
|
+
connectors/<connector-key>.ts
|
|
10
|
+
export interface FieldsInput { project: ProjectInput; summary: string; … }
|
|
11
|
+
export interface ProjectInput { key: string; }
|
|
12
|
+
export interface CreateIssueInputs { fields: FieldsInput; }
|
|
13
|
+
export interface Fields { issuetype?: Issuetype; }
|
|
14
|
+
export interface Issuetype { name?: string; }
|
|
15
|
+
export interface CreateIssueOutputs { fields?: Fields; id?: string; key?: string; … }
|
|
16
|
+
export const CreateIssue = descriptor<CreateIssueInputs, CreateIssueOutputs>()({
|
|
17
|
+
nodeType: 'uipath.connector.uipath-atlassian-jira.create-issue',
|
|
18
|
+
version: '1.0.0', requiresConnection: true, requiresFolderKey: true,
|
|
19
|
+
} as const);
|
|
20
|
+
|
|
21
|
+
Inputs are the union of the op's **v1def.inputDefinition.fields** (body) and
|
|
22
|
+
**entry.operation.parameters** (query/path/multipart). When both declarations are empty
|
|
23
|
+
but the catalog still declares required inputs, **entry.inputSchema.fields** is
|
|
24
|
+
the only available authoring surface and is used as a fallback. Catalog file
|
|
25
|
+
fields also fill older snapshots that omitted their multipart parameter. Dotted
|
|
26
|
+
and wildcard body fields are reconstructed as natural nested objects/arrays; `serialize()`/
|
|
27
|
+
`buildConnectorInputs` project them back to the exact registry field ids and
|
|
28
|
+
route every field to its declared Flow JSON slot. Outputs come from the entry's
|
|
29
|
+
`outputSchema.fields` and use the same nested reusable-type model.
|
|
30
|
+
|
|
31
|
+
**GenericExpanded ops are skipped for the bulk static library by default** to
|
|
32
|
+
avoid emitting thousands of descriptors. A small connection-resolved overlay
|
|
33
|
+
passes `--include-generic-expanded`; those entries carry the selected object's
|
|
34
|
+
live fields in `v1def.inputDefinition.fields`, and their descriptors bake in
|
|
35
|
+
`ConnectorMeta.objectName` so authoring needs no separate `{ object }` option.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
generate_connectors_ts.py --library <json-lib-dir> --output <ts-dir>
|
|
39
|
+
[--import <specifier>] [--connectors a,b,c]
|
|
40
|
+
[--include-generic-expanded]
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import json
|
|
46
|
+
import re
|
|
47
|
+
import sys
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
# Symbol derivation is SHARED with convert_library_to_md.py — the markdown cites
|
|
51
|
+
# these names, and a name derived twice is a name that drifts. See _naming.py.
|
|
52
|
+
from _naming import by_aliases, pascal, singular, symbol_index
|
|
53
|
+
from _lookups import classify, load_invariant_collections, reference_of
|
|
54
|
+
|
|
55
|
+
TYPE_MAP = {
|
|
56
|
+
"string": "string", "number": "number", "integer": "number", "float": "number",
|
|
57
|
+
"double": "number", "long": "number",
|
|
58
|
+
"boolean": "boolean", "bool": "boolean", "object": "Record<string, unknown>",
|
|
59
|
+
"array": "unknown[]", "json": "unknown", "file": "string",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_container_part(parameter: dict) -> bool:
|
|
64
|
+
"""A multipart parameter that is not a file.
|
|
65
|
+
|
|
66
|
+
The runtime composes it from ``bodyParameters`` (Teams ``body`` from
|
|
67
|
+
``body.content``, Gmail ``body`` from ``Body``, GenAI ``RagRequest`` from
|
|
68
|
+
``prompt``); the author never fills it, and a value written into it is
|
|
69
|
+
ignored at dispatch (verified live 2026-09-02). It is listed in the emitted
|
|
70
|
+
``multipartParameters`` by the compiler, not offered as an input here — the
|
|
71
|
+
same rule as ``core/library.ts``'s ``isContainerPart``.
|
|
72
|
+
"""
|
|
73
|
+
return (
|
|
74
|
+
parameter.get("type") == "multipart"
|
|
75
|
+
and str(parameter.get("dataType") or "string").lower() != "file"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def authoritative_input_fields(entry: dict, v1def: dict) -> list[dict]:
|
|
80
|
+
"""Return the same complete input vocabulary consumed by the SDK runtime.
|
|
81
|
+
|
|
82
|
+
Body fields live in ``v1def.inputDefinition.fields``; query, path, and
|
|
83
|
+
FILE multipart parameters live in ``entry.operation.parameters``. A
|
|
84
|
+
declaration wins when the same name appears in both because its wire
|
|
85
|
+
location is authoritative. A string-typed multipart parameter is the
|
|
86
|
+
transport container, not an input (see ``is_container_part``).
|
|
87
|
+
"""
|
|
88
|
+
fields = [dict(field) for field in
|
|
89
|
+
(((v1def.get("inputDefinition") or {}).get("fields")) or [])]
|
|
90
|
+
positions = {field.get("name"): index for index, field in enumerate(fields)}
|
|
91
|
+
parameters = ((entry.get("operation") or {}).get("parameters")) or []
|
|
92
|
+
for parameter in parameters:
|
|
93
|
+
name = parameter.get("name")
|
|
94
|
+
if not name or parameter.get("type") not in {
|
|
95
|
+
"query", "path", "multipart"
|
|
96
|
+
}:
|
|
97
|
+
continue
|
|
98
|
+
if is_container_part(parameter):
|
|
99
|
+
continue
|
|
100
|
+
field = {
|
|
101
|
+
"name": name,
|
|
102
|
+
"type": parameter.get("dataType"),
|
|
103
|
+
"required": parameter.get("required") is True,
|
|
104
|
+
}
|
|
105
|
+
if parameter.get("displayName"):
|
|
106
|
+
field["displayName"] = parameter["displayName"]
|
|
107
|
+
if parameter.get("description"):
|
|
108
|
+
field["description"] = parameter["description"]
|
|
109
|
+
# A QUERY parameter can carry a `reference` too — Slack's `send_as`
|
|
110
|
+
# does. Rebuilding the field from scratch dropped it, which made those
|
|
111
|
+
# lookups invisible to everything downstream.
|
|
112
|
+
if isinstance(parameter.get("reference"), dict):
|
|
113
|
+
field["reference"] = parameter["reference"]
|
|
114
|
+
if name in positions:
|
|
115
|
+
fields[positions[name]] = field
|
|
116
|
+
else:
|
|
117
|
+
positions[name] = len(fields)
|
|
118
|
+
fields.append(field)
|
|
119
|
+
return fields
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def input_fields(entry: dict, v1def: dict) -> list[dict]:
|
|
123
|
+
fields = authoritative_input_fields(entry, v1def)
|
|
124
|
+
catalog_fields = (entry.get("inputSchema") or {}).get("fields") or []
|
|
125
|
+
if not fields and any(
|
|
126
|
+
field.get("required") is True for field in catalog_fields
|
|
127
|
+
):
|
|
128
|
+
fields = [dict(field) for field in catalog_fields]
|
|
129
|
+
positions = {field.get("name"): index for index, field in enumerate(fields)}
|
|
130
|
+
for catalog_field in catalog_fields:
|
|
131
|
+
name = catalog_field.get("name")
|
|
132
|
+
if (
|
|
133
|
+
not name
|
|
134
|
+
or str(catalog_field.get("type") or "").lower() != "file"
|
|
135
|
+
or name in positions
|
|
136
|
+
):
|
|
137
|
+
continue
|
|
138
|
+
positions[name] = len(fields)
|
|
139
|
+
fields.append(dict(catalog_field))
|
|
140
|
+
return fields
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
EVENT_NODE_PREFIXES = ("uipath.connector.event.", "uipath.connector.trigger.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_event_node(node_type: str) -> bool:
|
|
147
|
+
"""An event/trigger entry, as opposed to a connector ACTION."""
|
|
148
|
+
return str(node_type or "").startswith(EVENT_NODE_PREFIXES)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def descriptor_input_fields(entry: dict, v1def: dict) -> list[dict]:
|
|
152
|
+
"""The one field list a descriptor's inputs — or an event's `where` — come from.
|
|
153
|
+
|
|
154
|
+
An event's authored inputs are its SUBSCRIPTION PARAMETERS, and
|
|
155
|
+
`v1def.eventParameters.fields` is the only place that holds them on BOTH
|
|
156
|
+
library shapes. A published entry happens to repeat them in
|
|
157
|
+
`inputDefinition.fields`, so reading `input_fields()` for an event looked
|
|
158
|
+
right; the overlay `prepare` writes leaves `inputDefinition.fields` empty
|
|
159
|
+
and keeps the parameters in the sidecar alone. The emit path read
|
|
160
|
+
`input_fields()` while the loop that builds the type tree read
|
|
161
|
+
`eventParameters` — two rules for one list — so after a `prepare`, and only
|
|
162
|
+
after a `prepare`, `where` degraded to `Record<string, string>` and the
|
|
163
|
+
descriptor carried no `lookups`. That is the descriptor `prepare` tells the
|
|
164
|
+
author to import, so `lookup(EmailReceived, 'parentFolderId')
|
|
165
|
+
.byDisplayName('Inbox')` failed with a bare TypeError on exactly the path
|
|
166
|
+
the skill steers them down.
|
|
167
|
+
|
|
168
|
+
An entry that declares none keeps the loose `where` (see `emit_event`).
|
|
169
|
+
"""
|
|
170
|
+
if is_event_node(entry.get("nodeType") or ""):
|
|
171
|
+
return (v1def.get("eventParameters") or {}).get("fields") or []
|
|
172
|
+
return input_fields(entry, v1def)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def required_input_gap(
|
|
176
|
+
entry: dict, input_tree: dict[str, "TypeNode"]
|
|
177
|
+
) -> str | None:
|
|
178
|
+
required = [
|
|
179
|
+
str(field.get("name") or "<unnamed>")
|
|
180
|
+
for field in (entry.get("inputSchema") or {}).get("fields") or []
|
|
181
|
+
if field.get("required") is True
|
|
182
|
+
]
|
|
183
|
+
if not required or input_tree:
|
|
184
|
+
return None
|
|
185
|
+
return (
|
|
186
|
+
f'{entry.get("nodeType", "<unknown node>")}: catalog requires '
|
|
187
|
+
f'{", ".join(required)}, but the generated inputs interface is empty'
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def ts_type(field: dict) -> str:
|
|
192
|
+
return TYPE_MAP.get(str(field.get("type") or "").lower(), "unknown")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TypeNode:
|
|
196
|
+
"""One node in a tree reconstructed from flattened connector field paths."""
|
|
197
|
+
|
|
198
|
+
def __init__(self, *, is_array: bool = False) -> None:
|
|
199
|
+
self.array_modes: set[bool] = {is_array}
|
|
200
|
+
self.declared_types: set[str] = set()
|
|
201
|
+
self.declared: dict | None = None
|
|
202
|
+
self.opaque_object = False
|
|
203
|
+
self.opaque_array = False
|
|
204
|
+
self.required = False
|
|
205
|
+
self.children: dict[str, TypeNode] = {}
|
|
206
|
+
|
|
207
|
+
def add_declaration(self, field: dict) -> None:
|
|
208
|
+
if self.declared is None:
|
|
209
|
+
self.declared = field
|
|
210
|
+
declared_type = str(field.get("type") or "").lower()
|
|
211
|
+
self.declared_types.add(ts_type(field))
|
|
212
|
+
self.opaque_object = self.opaque_object or declared_type == "object"
|
|
213
|
+
self.opaque_array = self.opaque_array or declared_type == "array"
|
|
214
|
+
self.required = self.required or bool(field.get("required"))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def split_field_path(name: str) -> list[tuple[str, bool]]:
|
|
218
|
+
"""Split ``items[*].owner.id`` into segments plus array cardinality."""
|
|
219
|
+
parts: list[tuple[str, bool]] = []
|
|
220
|
+
for raw in name.split("."):
|
|
221
|
+
is_array = raw.endswith("[*]")
|
|
222
|
+
segment = raw[:-3] if is_array else raw
|
|
223
|
+
# Preserve malformed/opaque registry names as one quoted property.
|
|
224
|
+
if not segment:
|
|
225
|
+
return [(name, False)]
|
|
226
|
+
parts.append((segment, is_array))
|
|
227
|
+
return parts
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def build_type_tree(fields: list[dict]) -> dict[str, TypeNode]:
|
|
231
|
+
roots: dict[str, TypeNode] = {}
|
|
232
|
+
for field in fields:
|
|
233
|
+
name = field.get("name")
|
|
234
|
+
if not name:
|
|
235
|
+
continue
|
|
236
|
+
cursor = roots
|
|
237
|
+
node: TypeNode | None = None
|
|
238
|
+
for segment, is_array in split_field_path(str(name)):
|
|
239
|
+
node = cursor.setdefault(segment, TypeNode(is_array=is_array))
|
|
240
|
+
node.array_modes.add(is_array)
|
|
241
|
+
cursor = node.children
|
|
242
|
+
if node is not None:
|
|
243
|
+
node.add_declaration(field)
|
|
244
|
+
return roots
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def merge_type_nodes(target: TypeNode, source: TypeNode) -> None:
|
|
248
|
+
"""Merge repeated occurrences into one reusable connector-level type."""
|
|
249
|
+
target.array_modes.update(source.array_modes)
|
|
250
|
+
target.declared_types.update(source.declared_types)
|
|
251
|
+
target.opaque_object = target.opaque_object or source.opaque_object
|
|
252
|
+
target.opaque_array = target.opaque_array or source.opaque_array
|
|
253
|
+
target.required = target.required or source.required
|
|
254
|
+
if target.declared is None:
|
|
255
|
+
target.declared = source.declared
|
|
256
|
+
for child_name, source_child in source.children.items():
|
|
257
|
+
target_child = target.children.get(child_name)
|
|
258
|
+
if target_child is None:
|
|
259
|
+
target.children[child_name] = source_child
|
|
260
|
+
else:
|
|
261
|
+
merge_type_nodes(target_child, source_child)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def nested_type_name(field_name: str, node: TypeNode, suffix: str = "") -> str:
|
|
265
|
+
source = singular(field_name) if (True in node.array_modes or node.opaque_array) else field_name
|
|
266
|
+
name = pascal(source)
|
|
267
|
+
name = name if re.match(r"[A-Za-z_$]", name) else f"Nested{name}"
|
|
268
|
+
return f"{name}{suffix}"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def key_literal(name: str) -> str:
|
|
272
|
+
# A field name is a valid ident only if it has no dots/brackets/etc.
|
|
273
|
+
return name if re.fullmatch(r"[A-Za-z_$][A-Za-z0-9_$]*", name) else json.dumps(name)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def subtree_required(node: TypeNode) -> bool:
|
|
277
|
+
return node.required or any(subtree_required(child) for child in node.children.values())
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def type_fingerprint(node: TypeNode) -> tuple:
|
|
281
|
+
"""Stable structural identity; input types reuse only exact shapes."""
|
|
282
|
+
return (
|
|
283
|
+
tuple(sorted(node.array_modes)),
|
|
284
|
+
tuple(sorted(node.declared_types)),
|
|
285
|
+
node.opaque_object,
|
|
286
|
+
node.opaque_array,
|
|
287
|
+
node.required,
|
|
288
|
+
tuple((name, type_fingerprint(child)) for name, child in sorted(node.children.items())),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class NamedTypeRegistry:
|
|
293
|
+
"""Reusable named interfaces shared by all ops in one connector."""
|
|
294
|
+
|
|
295
|
+
def __init__(
|
|
296
|
+
self, *, suffix: str = "", all_optional: bool = True, merge_reused: bool = True,
|
|
297
|
+
is_input: bool = False,
|
|
298
|
+
) -> None:
|
|
299
|
+
self.is_input = is_input
|
|
300
|
+
self.suffix = suffix
|
|
301
|
+
self.all_optional = all_optional
|
|
302
|
+
self.merge_reused = merge_reused
|
|
303
|
+
self._by_name: dict[str, TypeNode] = {}
|
|
304
|
+
self._by_node: dict[int, str] = {}
|
|
305
|
+
self._shape_names: dict[tuple[str, tuple], str] = {}
|
|
306
|
+
self._definitions: list[str] = []
|
|
307
|
+
self._registering: set[str] = set()
|
|
308
|
+
|
|
309
|
+
def register(self, field_name: str, node: TypeNode) -> str:
|
|
310
|
+
known = self._by_node.get(id(node))
|
|
311
|
+
if known:
|
|
312
|
+
return known
|
|
313
|
+
|
|
314
|
+
base_name = nested_type_name(field_name, node, self.suffix)
|
|
315
|
+
if self.merge_reused:
|
|
316
|
+
type_name = base_name
|
|
317
|
+
else:
|
|
318
|
+
shape_key = (base_name, type_fingerprint(node))
|
|
319
|
+
type_name = self._shape_names.get(shape_key, "")
|
|
320
|
+
if not type_name:
|
|
321
|
+
type_name = base_name
|
|
322
|
+
suffix = 2
|
|
323
|
+
while type_name in self._by_name:
|
|
324
|
+
type_name = f"{base_name}{suffix}"
|
|
325
|
+
suffix += 1
|
|
326
|
+
self._shape_names[shape_key] = type_name
|
|
327
|
+
|
|
328
|
+
canonical = self._by_name.get(type_name)
|
|
329
|
+
if canonical is None:
|
|
330
|
+
canonical = node
|
|
331
|
+
self._by_name[type_name] = canonical
|
|
332
|
+
self._definitions.append(type_name)
|
|
333
|
+
elif canonical is not node:
|
|
334
|
+
if self.merge_reused:
|
|
335
|
+
merge_type_nodes(canonical, node)
|
|
336
|
+
self._by_node[id(node)] = type_name
|
|
337
|
+
|
|
338
|
+
# A schema can recursively repeat the same segment name
|
|
339
|
+
# (`children[*].children[*]...`). Merge it into the shared interface,
|
|
340
|
+
# but do not recursively rediscover that interface while it is active.
|
|
341
|
+
if type_name in self._registering:
|
|
342
|
+
return type_name
|
|
343
|
+
self._registering.add(type_name)
|
|
344
|
+
|
|
345
|
+
# Registration can enrich a canonical node, so iterate a snapshot.
|
|
346
|
+
try:
|
|
347
|
+
for child_name, child in list(canonical.children.items()):
|
|
348
|
+
if child.children or child.opaque_object:
|
|
349
|
+
self.register(child_name, child)
|
|
350
|
+
finally:
|
|
351
|
+
self._registering.remove(type_name)
|
|
352
|
+
return type_name
|
|
353
|
+
|
|
354
|
+
def discover(self, roots: dict[str, TypeNode]) -> None:
|
|
355
|
+
for field_name, node in roots.items():
|
|
356
|
+
if node.children or node.opaque_object:
|
|
357
|
+
self.register(field_name, node)
|
|
358
|
+
|
|
359
|
+
def _known_array_element(self, field_name: str, node: TypeNode) -> str | None:
|
|
360
|
+
candidate = nested_type_name(field_name, node, self.suffix)
|
|
361
|
+
matches = [
|
|
362
|
+
name for name in self._by_name
|
|
363
|
+
if name == candidate or re.fullmatch(re.escape(candidate) + r"[2-9][0-9]*", name)
|
|
364
|
+
]
|
|
365
|
+
return matches[0] if len(matches) == 1 else None
|
|
366
|
+
|
|
367
|
+
def type_for(self, field_name: str, node: TypeNode) -> str:
|
|
368
|
+
if node.children or node.opaque_object:
|
|
369
|
+
base = self.register(field_name, node)
|
|
370
|
+
elif node.opaque_array:
|
|
371
|
+
known_element = self._known_array_element(field_name, node)
|
|
372
|
+
if known_element:
|
|
373
|
+
return f"Array<{known_element}>"
|
|
374
|
+
return "unknown[]"
|
|
375
|
+
else:
|
|
376
|
+
base = " | ".join(sorted(node.declared_types)) or "unknown"
|
|
377
|
+
|
|
378
|
+
has_array = True in node.array_modes
|
|
379
|
+
has_scalar = False in node.array_modes
|
|
380
|
+
if has_array and has_scalar:
|
|
381
|
+
return f"{base} | Array<{base}>"
|
|
382
|
+
if has_array:
|
|
383
|
+
return f"Array<{base}>"
|
|
384
|
+
return base
|
|
385
|
+
|
|
386
|
+
def emit(self) -> list[str]:
|
|
387
|
+
lines: list[str] = []
|
|
388
|
+
# Rendering a definition may register an opaque nested object. Iterate
|
|
389
|
+
# by index so newly discovered definitions are emitted too.
|
|
390
|
+
index = 0
|
|
391
|
+
while index < len(self._definitions):
|
|
392
|
+
type_name = self._definitions[index]
|
|
393
|
+
node = self._by_name[type_name]
|
|
394
|
+
lines.append(f"export interface {type_name} {{")
|
|
395
|
+
if node.opaque_object:
|
|
396
|
+
lines.append(" [key: string]: unknown;")
|
|
397
|
+
for child_name, child in node.children.items():
|
|
398
|
+
lines += emit_nested_property(
|
|
399
|
+
child_name, child, " ", self, self.all_optional, self.is_input,
|
|
400
|
+
)
|
|
401
|
+
lines.append("}")
|
|
402
|
+
lines.append("")
|
|
403
|
+
index += 1
|
|
404
|
+
return lines
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def emit_nested_property(
|
|
408
|
+
name: str,
|
|
409
|
+
node: TypeNode,
|
|
410
|
+
indent: str,
|
|
411
|
+
named_types: NamedTypeRegistry,
|
|
412
|
+
all_optional: bool,
|
|
413
|
+
is_input: bool = False,
|
|
414
|
+
) -> list[str]:
|
|
415
|
+
"""One property line.
|
|
416
|
+
|
|
417
|
+
An INPUT property is wrapped so it also accepts an expression, and a
|
|
418
|
+
resolvable lookup additionally accepts an unresolved token. The wrapper goes
|
|
419
|
+
HERE rather than on the leaf type so that `tags: input('tags')` — the whole
|
|
420
|
+
collection as one expression — is legal alongside `tags: ['a', out(…)]`;
|
|
421
|
+
`ConnectorValue` distributes over arrays to cover both.
|
|
422
|
+
|
|
423
|
+
An OUTPUT property is never wrapped. It is a value that came back from the
|
|
424
|
+
connector, so it cannot be an expression or an unresolved lookup.
|
|
425
|
+
"""
|
|
426
|
+
lines: list[str] = []
|
|
427
|
+
declared = node.declared or {}
|
|
428
|
+
desc = str(declared.get("description") or "").strip().replace("\n", " ")
|
|
429
|
+
if is_input and declared.get("__lookup"):
|
|
430
|
+
# The identity goes ON the field, not only in the LOOKUP block above the
|
|
431
|
+
# descriptor: the field is where the value gets typed, and "The reporter
|
|
432
|
+
# of the issue" beside `ConnectorLookupValue<string>` read as "any string"
|
|
433
|
+
# — an e-mail went into `fields.reporter.id` and Jira refused it at run time.
|
|
434
|
+
desc = f"{desc} {lookup_field_note(declared)}".strip()
|
|
435
|
+
if desc:
|
|
436
|
+
lines.append(f"{indent}/** {desc} */")
|
|
437
|
+
optional = "?" if all_optional or not subtree_required(node) else ""
|
|
438
|
+
rendered = named_types.type_for(name, node)
|
|
439
|
+
if is_input:
|
|
440
|
+
wrapper = "ConnectorLookupValue" if declared.get("__lookup") else "ConnectorValue"
|
|
441
|
+
rendered = f"{wrapper}<{rendered}>"
|
|
442
|
+
lines.append(f"{indent}{key_literal(name)}{optional}: {rendered};")
|
|
443
|
+
return lines
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def lookup_field_note(declared: dict) -> str:
|
|
447
|
+
"""One sentence for a lookup field's own doc comment: what it sends, how to get one."""
|
|
448
|
+
reference = reference_of(declared) or {}
|
|
449
|
+
target = str(reference.get("objectName") or str(reference.get("path") or "").split("?", 1)[0])
|
|
450
|
+
value = str(reference.get("lookupValue") or "id")
|
|
451
|
+
field = str(declared.get("name") or "")
|
|
452
|
+
return (
|
|
453
|
+
f"LOOKUP — sends a `{target}.{value}`, never a name or e-mail: "
|
|
454
|
+
f"lookup(<Descriptor>, '{field}') (helpers in the LOOKUP lines above the descriptor)."
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def emit_interface(
|
|
459
|
+
name: str,
|
|
460
|
+
fields: list[dict],
|
|
461
|
+
all_optional: bool,
|
|
462
|
+
named_types: NamedTypeRegistry,
|
|
463
|
+
field_tree: dict[str, TypeNode],
|
|
464
|
+
is_input: bool = False,
|
|
465
|
+
) -> list[str]:
|
|
466
|
+
out = [f"export interface {name} {{"]
|
|
467
|
+
for field_name, node in field_tree.items():
|
|
468
|
+
out += emit_nested_property(
|
|
469
|
+
field_name, node, " ", named_types, all_optional, is_input,
|
|
470
|
+
)
|
|
471
|
+
out.append("}")
|
|
472
|
+
return out
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def lookup_strategy(reference: dict) -> str:
|
|
477
|
+
"""Which retrieval strategy `prepare` will use for this field.
|
|
478
|
+
|
|
479
|
+
Order matters. `dependsOn` wins outright because it is a PREREQUISITE
|
|
480
|
+
rather than an optimisation — the collection is not addressable at all
|
|
481
|
+
until another field resolves, so no amount of filtering helps. After that
|
|
482
|
+
a server-side filter beats a tree walk beats a full scan.
|
|
483
|
+
"""
|
|
484
|
+
if reference.get("dependsOn"):
|
|
485
|
+
return "dependent"
|
|
486
|
+
if reference.get("filterPattern"):
|
|
487
|
+
return "filter"
|
|
488
|
+
if reference.get("childPath"):
|
|
489
|
+
return "tree"
|
|
490
|
+
return "scan"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def lookup_specs(entry: dict, v1def: dict, invariant: set) -> dict:
|
|
494
|
+
"""`{fieldName: spec}` for every field that earns a `.by*` helper.
|
|
495
|
+
|
|
496
|
+
Keyed by the WIRE field name — the same string `buildConnectorInputs`
|
|
497
|
+
matches on and the same one `prepare --resolve <field>:…` takes — so a
|
|
498
|
+
nested field like `fields.project.key` is addressed by its dotted path and
|
|
499
|
+
there is exactly one spelling of a field across the whole toolchain.
|
|
500
|
+
|
|
501
|
+
`reference` is unioned in from `inputSchema.fields` where the authoring
|
|
502
|
+
vocabulary lacks it: the two declarations disagree about which fields they
|
|
503
|
+
carry, and a lookup declared only in the catalog is still a lookup.
|
|
504
|
+
"""
|
|
505
|
+
catalog_references = {
|
|
506
|
+
f.get("name"): f["reference"]
|
|
507
|
+
for f in ((entry.get("inputSchema") or {}).get("fields") or [])
|
|
508
|
+
if isinstance(f.get("reference"), dict)
|
|
509
|
+
}
|
|
510
|
+
return specs_from_fields(
|
|
511
|
+
input_fields(entry, v1def), invariant, catalog_references)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def specs_from_fields(
|
|
515
|
+
fields: list[dict], invariant: set, catalog_references: dict | None = None,
|
|
516
|
+
) -> dict:
|
|
517
|
+
"""`{fieldName: spec}` for the lookup-bearing fields of ANY field list.
|
|
518
|
+
|
|
519
|
+
Split out of `lookup_specs` so a connector EVENT can build the same specs
|
|
520
|
+
from its `eventParameters.fields` — the list `lookup()` resolves against for
|
|
521
|
+
a subscription — instead of from `inputSchema`/`inputDefinition`, which an
|
|
522
|
+
event does not use. Two emitters, one implementation: when they were
|
|
523
|
+
separate, `emit_event` simply had none, and every event descriptor shipped
|
|
524
|
+
without the `.by*` helpers its curated sibling had.
|
|
525
|
+
"""
|
|
526
|
+
specs = {}
|
|
527
|
+
for field in fields:
|
|
528
|
+
name = field.get("name")
|
|
529
|
+
if not name:
|
|
530
|
+
continue
|
|
531
|
+
merged = dict(field)
|
|
532
|
+
if (not isinstance(merged.get("reference"), dict)
|
|
533
|
+
and catalog_references and name in catalog_references):
|
|
534
|
+
merged["reference"] = catalog_references[name]
|
|
535
|
+
if classify(merged, invariant) != "lookup":
|
|
536
|
+
continue
|
|
537
|
+
reference = reference_of(merged)
|
|
538
|
+
names = [str(n) for n in (reference.get("lookupNames") or [])]
|
|
539
|
+
spec = {
|
|
540
|
+
"path": reference.get("path") or "",
|
|
541
|
+
"by": names,
|
|
542
|
+
"value": str(reference.get("lookupValue")),
|
|
543
|
+
"aliases": by_aliases(names),
|
|
544
|
+
"strategy": lookup_strategy(reference),
|
|
545
|
+
}
|
|
546
|
+
if reference.get("objectName"):
|
|
547
|
+
spec["objectName"] = str(reference["objectName"])
|
|
548
|
+
if reference.get("dependsOn"):
|
|
549
|
+
spec["dependsOn"] = [str(d) for d in reference["dependsOn"]]
|
|
550
|
+
specs[name] = spec
|
|
551
|
+
return specs
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def mark_lookup_fields(entry: dict, fields: list[dict], invariant: set) -> list[dict]:
|
|
555
|
+
"""Flag the fields whose TS type must also accept a `LookupToken`.
|
|
556
|
+
|
|
557
|
+
Exactly the set `lookup_specs` generates helpers for, decided by the same
|
|
558
|
+
call, so the type and the helper cannot disagree about which fields are
|
|
559
|
+
resolvable.
|
|
560
|
+
"""
|
|
561
|
+
catalog_references = {
|
|
562
|
+
f.get("name"): f["reference"]
|
|
563
|
+
for f in ((entry.get("inputSchema") or {}).get("fields") or [])
|
|
564
|
+
if isinstance(f.get("reference"), dict)
|
|
565
|
+
}
|
|
566
|
+
out = []
|
|
567
|
+
for field in fields:
|
|
568
|
+
merged = dict(field)
|
|
569
|
+
name = merged.get("name")
|
|
570
|
+
if not isinstance(merged.get("reference"), dict) and name in catalog_references:
|
|
571
|
+
merged["reference"] = catalog_references[name]
|
|
572
|
+
if classify(merged, invariant) == "lookup":
|
|
573
|
+
merged["__lookup"] = True
|
|
574
|
+
out.append(merged)
|
|
575
|
+
return out
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def emit_lookup_comment(specs: dict, symbol: str = "<Descriptor>") -> list[str]:
|
|
579
|
+
"""The grep-able header — route 3 of the design's discoverability section.
|
|
580
|
+
|
|
581
|
+
The agents this surface is for run with Bash/Read/Grep and no language
|
|
582
|
+
server, so a `.byEmail` discoverable only by hovering is invisible. The
|
|
583
|
+
descriptor module is real source and is already opened to check a name, so
|
|
584
|
+
the lookups are stated there in prose beside the data.
|
|
585
|
+
"""
|
|
586
|
+
lines = []
|
|
587
|
+
for field, spec in specs.items():
|
|
588
|
+
target = str(spec.get("objectName") or spec["path"]).split("?", 1)[0]
|
|
589
|
+
lines.append(f" * LOOKUP: `{field}` -> {target}.{spec['value']}")
|
|
590
|
+
aliases = spec["aliases"]
|
|
591
|
+
offered = [f"{a}()" for a in aliases.values()] if aliases else []
|
|
592
|
+
by_list = ", ".join(f"`{n}`" for n in spec["by"])
|
|
593
|
+
lines.append(f" * by: {by_list}")
|
|
594
|
+
# THE WHOLE CALL, not just the method names. Naming `.byEmail` without
|
|
595
|
+
# the `lookup(Descriptor, 'field')` wrapper tells a reader that helpers
|
|
596
|
+
# exist but not how to reach one — measured: an agent read this comment,
|
|
597
|
+
# learned the helper names, and then made three separate reads of
|
|
598
|
+
# api.md to find the signature. One line here removes that trip.
|
|
599
|
+
first = next(iter(aliases.values()), None)
|
|
600
|
+
call = (f"lookup({symbol}, '{field}').{first}(value)" if first
|
|
601
|
+
else f"lookup({symbol}, '{field}').by('{spec['by'][0]}', value)"
|
|
602
|
+
if spec["by"] else f"lookup({symbol}, '{field}')")
|
|
603
|
+
lines.append(f" * use: {call}")
|
|
604
|
+
if offered:
|
|
605
|
+
lines.append(f" * also: {', '.join('.' + a for a in aliases.values())}"
|
|
606
|
+
f" · .by(name, value)")
|
|
607
|
+
else:
|
|
608
|
+
lines.append(" * also: .by(name, value) only — aliases collided")
|
|
609
|
+
lines.append(f" * strategy: {STRATEGY_PROSE[spec['strategy']]}")
|
|
610
|
+
if spec.get("dependsOn"):
|
|
611
|
+
lines.append(f" * resolve first: {', '.join('`' + d + '`' for d in spec['dependsOn'])}")
|
|
612
|
+
return lines
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
STRATEGY_PROSE = {
|
|
616
|
+
"filter": "one request (the operation declares a server-side filter)",
|
|
617
|
+
"scan": "paged scan (this operation declares no filterPattern)",
|
|
618
|
+
"tree": "hierarchical walk (the collection is a tree)",
|
|
619
|
+
"dependent": "dependent — another field must resolve first",
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def emit_event(
|
|
624
|
+
entry: dict,
|
|
625
|
+
in_fields: list[dict],
|
|
626
|
+
out_fields: list[dict],
|
|
627
|
+
input_tree: dict[str, TypeNode],
|
|
628
|
+
output_tree: dict[str, TypeNode],
|
|
629
|
+
name: str,
|
|
630
|
+
input_types: NamedTypeRegistry,
|
|
631
|
+
output_types: NamedTypeRegistry,
|
|
632
|
+
invariant: set | None = None,
|
|
633
|
+
) -> list[str]:
|
|
634
|
+
"""Emit a connector EVENT as a `triggerDescriptor()`, the shape `onEvent()` takes.
|
|
635
|
+
|
|
636
|
+
An event entry (`uipath.connector.event.<key>.<event>`, or the curated
|
|
637
|
+
`.trigger.` sibling) is not an action: `onEvent`/`waitForEvent` identify it by
|
|
638
|
+
`connector` + `event`, and a `descriptor()` const — the action shape this
|
|
639
|
+
generator used to emit for it — fails their positive descriptor test, so the
|
|
640
|
+
subscription lost its connector and event (EVENT_NO_CONNECTOR/EVENT_NO_EVENT)
|
|
641
|
+
and every author hand-wrote a `triggerDescriptor()` instead. The payload type
|
|
642
|
+
comes from `outputSchema`, and `where` from the entry's event parameters —
|
|
643
|
+
a GENERIC event's object is the author's `object` option, not a field here.
|
|
644
|
+
|
|
645
|
+
`where` was `Record<string, string>` while an event entry carried no
|
|
646
|
+
parameters to type it from. It does now, so a misspelt parameter is a
|
|
647
|
+
compile error rather than a `check` warning. The fallback stays for an
|
|
648
|
+
entry that declares NONE: that is either an event which genuinely takes no
|
|
649
|
+
parameters (HTTP Webhook — the connection is the subscription's whole
|
|
650
|
+
scope) or one nothing has described yet, and an empty interface would
|
|
651
|
+
refuse every key in both cases — wrongly in the second.
|
|
652
|
+
"""
|
|
653
|
+
node_type = entry["nodeType"]
|
|
654
|
+
event_id = node_type.rsplit(".", 1)[-1]
|
|
655
|
+
connector_key = (entry.get("connector") or {}).get("key") or ""
|
|
656
|
+
label = (entry.get("display") or {}).get("label") or event_id
|
|
657
|
+
version = entry.get("version", "1.0.0")
|
|
658
|
+
generic = (entry.get("operation") or {}).get("activityType") == "GenericTrigger"
|
|
659
|
+
lines: list[str] = []
|
|
660
|
+
where_type = "Record<string, string>"
|
|
661
|
+
if in_fields:
|
|
662
|
+
where_type = f"{name}Where"
|
|
663
|
+
lines += emit_interface(
|
|
664
|
+
where_type, in_fields, all_optional=False,
|
|
665
|
+
named_types=input_types, field_tree=input_tree, is_input=True,
|
|
666
|
+
)
|
|
667
|
+
lines.append("")
|
|
668
|
+
lines += emit_interface(
|
|
669
|
+
f"{name}Outputs", out_fields, all_optional=True,
|
|
670
|
+
named_types=output_types, field_tree=output_tree,
|
|
671
|
+
)
|
|
672
|
+
lines.append("")
|
|
673
|
+
# A subscription's `where` values are resolved by `lookup()` exactly as a
|
|
674
|
+
# connector input's are, and `prepare --resolve` reads the same reference
|
|
675
|
+
# block — so the descriptor has to carry the same specs. Emitting none is
|
|
676
|
+
# what made `lookup(EmailReceived, 'parentFolderId').byDisplayName('Inbox')`
|
|
677
|
+
# fail with a bare TypeError while the curated sibling's `.byDisplayName`
|
|
678
|
+
# worked, on a field both forms declare identically.
|
|
679
|
+
specs = specs_from_fields(in_fields, invariant or set())
|
|
680
|
+
lines.append("/**")
|
|
681
|
+
lines.append(f" * {label} — {node_type}")
|
|
682
|
+
lines.append(" *")
|
|
683
|
+
if generic:
|
|
684
|
+
lines.append(" * A GENERIC event: it fires for ONE object of the connection (an entity, a")
|
|
685
|
+
lines.append(" * table, a custom object), so name it — `onEvent(" + name + ", { object: '<Object>' })`.")
|
|
686
|
+
lines.append(f" * `uip is triggers objects {connector_key} <EVENT> --connection-id <id>` lists them.")
|
|
687
|
+
else:
|
|
688
|
+
lines.append(f" * Start a flow on it with `onEvent({name}, {{ where: {{ … }} }})`, or pause for it")
|
|
689
|
+
lines.append(f" * with `waitForEvent({name}, {{ … }})`; `where` carries its event parameters.")
|
|
690
|
+
# INSIDE the block, as `emit_op` does. `emit_lookup_comment` returns bare
|
|
691
|
+
# ` * ` body lines; prepending them to the module instead emitted a comment
|
|
692
|
+
# with no `/**` opener, and every connector module holding an event with a
|
|
693
|
+
# lookup stopped parsing — which no assertion on substrings could see.
|
|
694
|
+
if specs:
|
|
695
|
+
lines.append(" *")
|
|
696
|
+
lines += emit_lookup_comment(specs, name)
|
|
697
|
+
lines.append(" */")
|
|
698
|
+
lines.append(f"export const {name} = triggerDescriptor<{where_type}, {name}Outputs>()({{")
|
|
699
|
+
lines.append(f" connector: {json.dumps(connector_key)},")
|
|
700
|
+
lines.append(f" event: {json.dumps(event_id)},")
|
|
701
|
+
lines.append(f" version: {json.dumps(version)},")
|
|
702
|
+
lines.append(f" displayName: {json.dumps(label)},")
|
|
703
|
+
lines += emit_lookups_block(specs)
|
|
704
|
+
lines.append("} as const);")
|
|
705
|
+
return lines
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def emit_op(
|
|
709
|
+
entry: dict,
|
|
710
|
+
v1def: dict,
|
|
711
|
+
input_tree: dict[str, TypeNode],
|
|
712
|
+
output_tree: dict[str, TypeNode],
|
|
713
|
+
name: str,
|
|
714
|
+
input_types: NamedTypeRegistry,
|
|
715
|
+
output_types: NamedTypeRegistry,
|
|
716
|
+
invariant: set | None = None,
|
|
717
|
+
) -> list[str]:
|
|
718
|
+
node_type = entry["nodeType"]
|
|
719
|
+
action_id = node_type.rsplit(".", 1)[-1]
|
|
720
|
+
operation = entry.get("operation") or {}
|
|
721
|
+
is_generic_expanded = operation.get("activityType") == "GenericExpanded"
|
|
722
|
+
label = (entry.get("display") or {}).get("label") or action_id
|
|
723
|
+
version = entry.get("version", "1.0.0")
|
|
724
|
+
runtime = entry.get("runtime", {}) or {}
|
|
725
|
+
in_fields = descriptor_input_fields(entry, v1def)
|
|
726
|
+
out_fields = (entry.get("outputSchema") or {}).get("fields") or []
|
|
727
|
+
|
|
728
|
+
if is_event_node(node_type):
|
|
729
|
+
return emit_event(entry, in_fields, out_fields, input_tree, output_tree,
|
|
730
|
+
name, input_types, output_types, invariant)
|
|
731
|
+
|
|
732
|
+
lines: list[str] = []
|
|
733
|
+
lines += emit_interface(
|
|
734
|
+
f"{name}Inputs", in_fields, all_optional=False,
|
|
735
|
+
named_types=input_types, field_tree=input_tree, is_input=True,
|
|
736
|
+
)
|
|
737
|
+
lines.append("")
|
|
738
|
+
lines += emit_interface(
|
|
739
|
+
f"{name}Outputs",
|
|
740
|
+
out_fields,
|
|
741
|
+
all_optional=True,
|
|
742
|
+
named_types=output_types,
|
|
743
|
+
field_tree=output_tree,
|
|
744
|
+
)
|
|
745
|
+
lines.append("")
|
|
746
|
+
# An operation whose real field set arrives at design time has an `Inputs`
|
|
747
|
+
# type that is either empty (no object chosen yet) or just the parents.
|
|
748
|
+
# Nothing in the type says so, and the Markdown page that does say it is a
|
|
749
|
+
# different artifact — so a developer reading types alone concludes the
|
|
750
|
+
# operation takes exactly what is written here. The doc comment is the only
|
|
751
|
+
# place the typed surface can carry it.
|
|
752
|
+
schema_action = operation.get("schemaAction") or {}
|
|
753
|
+
specs = lookup_specs(entry, v1def, invariant or set())
|
|
754
|
+
lookup_comment = emit_lookup_comment(specs, name) if specs else []
|
|
755
|
+
if operation.get("objectFromDiscovery"):
|
|
756
|
+
lines.append("/**")
|
|
757
|
+
lines.append(f" * {label} — {node_type}")
|
|
758
|
+
lines.append(" *")
|
|
759
|
+
lines.append(" * The object is chosen against the connection, so `Inputs` is empty")
|
|
760
|
+
lines.append(" * here. List the objects and read the chosen one's fields with")
|
|
761
|
+
lines.append(" * `uip is resources list` / `describe` before binding this node.")
|
|
762
|
+
if lookup_comment:
|
|
763
|
+
lines.append(" *")
|
|
764
|
+
lines += lookup_comment
|
|
765
|
+
lines.append(" */")
|
|
766
|
+
elif schema_action.get("parents"):
|
|
767
|
+
parents = ", ".join(f"`{x}`" for x in schema_action["parents"])
|
|
768
|
+
lines.append("/**")
|
|
769
|
+
lines.append(f" * {label} — {node_type}")
|
|
770
|
+
lines.append(" *")
|
|
771
|
+
lines.append(f" * `Inputs` carries only the fields that SELECT the schema ({parents}).")
|
|
772
|
+
lines.append(f" * The real set comes from the `{schema_action.get('name')}` design-time")
|
|
773
|
+
lines.append(" * action — describe with those values supplied to see it.")
|
|
774
|
+
if lookup_comment:
|
|
775
|
+
lines.append(" *")
|
|
776
|
+
lines += lookup_comment
|
|
777
|
+
lines.append(" */")
|
|
778
|
+
elif lookup_comment:
|
|
779
|
+
lines.append("/**")
|
|
780
|
+
lines.append(f" * {label} — {node_type}")
|
|
781
|
+
lines.append(" *")
|
|
782
|
+
lines += lookup_comment
|
|
783
|
+
lines.append(" */")
|
|
784
|
+
else:
|
|
785
|
+
lines.append(f"/** {label} — {node_type} */")
|
|
786
|
+
lines.append(f"export const {name} = descriptor<{name}Inputs, {name}Outputs>()({{")
|
|
787
|
+
lines.append(f" nodeType: {json.dumps(node_type)},")
|
|
788
|
+
lines.append(f" version: {json.dumps(version)},")
|
|
789
|
+
if is_generic_expanded and operation.get("objectName"):
|
|
790
|
+
lines.append(f" objectName: {json.dumps(operation['objectName'])},")
|
|
791
|
+
if runtime.get("requiresConnection"):
|
|
792
|
+
lines.append(" requiresConnection: true,")
|
|
793
|
+
if runtime.get("requiresFolderKey"):
|
|
794
|
+
lines.append(" requiresFolderKey: true,")
|
|
795
|
+
lines += emit_lookups_block(specs)
|
|
796
|
+
lines.append("} as const);")
|
|
797
|
+
return lines
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def emit_lookups_block(specs: dict) -> list[str]:
|
|
801
|
+
"""The descriptor's `lookups:` literal — a RUNTIME value, not a type.
|
|
802
|
+
|
|
803
|
+
`lookup()` reads `aliases` from it to build the `.byName()` family, so a
|
|
804
|
+
descriptor emitted without this block offers only the generic
|
|
805
|
+
`.by(name, value)` and any alias call fails as a plain `TypeError`.
|
|
806
|
+
"""
|
|
807
|
+
if not specs:
|
|
808
|
+
return []
|
|
809
|
+
lines = [" lookups: {"]
|
|
810
|
+
for field, spec in specs.items():
|
|
811
|
+
lines.append(f" {key_literal(field)}: {{")
|
|
812
|
+
if spec.get("objectName"):
|
|
813
|
+
lines.append(f" objectName: {json.dumps(spec['objectName'])},")
|
|
814
|
+
lines.append(f" path: {json.dumps(spec['path'])},")
|
|
815
|
+
lines.append(f" by: {json.dumps(spec['by'])},")
|
|
816
|
+
lines.append(f" value: {json.dumps(spec['value'])},")
|
|
817
|
+
aliases = ", ".join(
|
|
818
|
+
f"{alias}: {json.dumps(name)}" for name, alias in spec["aliases"].items()
|
|
819
|
+
)
|
|
820
|
+
lines.append(f" aliases: {{{(' ' + aliases + ' ') if aliases else ''}}},")
|
|
821
|
+
lines.append(f" strategy: {json.dumps(spec['strategy'])},")
|
|
822
|
+
if spec.get("dependsOn"):
|
|
823
|
+
lines.append(f" dependsOn: {json.dumps(spec['dependsOn'])},")
|
|
824
|
+
lines.append(" },")
|
|
825
|
+
lines.append(" },")
|
|
826
|
+
return lines
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def main() -> None:
|
|
830
|
+
ap = argparse.ArgumentParser()
|
|
831
|
+
ap.add_argument("--library", required=True)
|
|
832
|
+
ap.add_argument("--output", required=True)
|
|
833
|
+
ap.add_argument("--import", dest="imp", default="@uipath/maestro-builder-sdk",
|
|
834
|
+
help="module specifier for `import { descriptor }`")
|
|
835
|
+
ap.add_argument("--connectors", default="", help="csv subset of connector keys (default: all)")
|
|
836
|
+
ap.add_argument(
|
|
837
|
+
"--include-generic-expanded", action="store_true",
|
|
838
|
+
help="emit typed descriptors for GenericExpanded entries (used by "
|
|
839
|
+
"small connection-resolved overlays; off for the bulk library)",
|
|
840
|
+
)
|
|
841
|
+
args = ap.parse_args()
|
|
842
|
+
|
|
843
|
+
lib = Path(args.library)
|
|
844
|
+
idx = json.loads((lib / "index.json").read_text())
|
|
845
|
+
# The invariant-collection ledger sits beside the index. Absent reads as
|
|
846
|
+
# empty, which is the permissive pre-ledger behaviour (see _lookups.py).
|
|
847
|
+
invariant = load_invariant_collections(lib)
|
|
848
|
+
subset = {c for c in args.connectors.split(",") if c} or None
|
|
849
|
+
|
|
850
|
+
# group Curated + http-request entries by connector key
|
|
851
|
+
by_conn: dict[str, list[dict]] = {}
|
|
852
|
+
for e in idx["entries"]:
|
|
853
|
+
if e.get("activityType") == "GenericExpanded" and not args.include_generic_expanded:
|
|
854
|
+
continue
|
|
855
|
+
# A generic EVENT's per-object variants share their template's nodeType
|
|
856
|
+
# and so would emit the same const twice — a module that does not
|
|
857
|
+
# compile. Only the template gets a descriptor, which is also the right
|
|
858
|
+
# surface: `onEvent(RecordCreated, { object: 'Message' })` names the
|
|
859
|
+
# object as an OPTION, so one descriptor serves every object. The
|
|
860
|
+
# variants still back `check` and `compile`, which resolve by object.
|
|
861
|
+
if (e.get("activityType") == "GenericTrigger" and e.get("objectName")
|
|
862
|
+
and not args.include_generic_expanded):
|
|
863
|
+
continue
|
|
864
|
+
ck = e["connectorKey"]
|
|
865
|
+
if subset and ck not in subset:
|
|
866
|
+
continue
|
|
867
|
+
by_conn.setdefault(ck, []).append(e)
|
|
868
|
+
|
|
869
|
+
out_dir = Path(args.output)
|
|
870
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
871
|
+
written = 0
|
|
872
|
+
required_input_gaps: list[str] = []
|
|
873
|
+
# ONE symbol map for the whole run, shared with convert_library_to_md.py so
|
|
874
|
+
# the markdown can cite these names without re-deriving them (see
|
|
875
|
+
# _naming.symbol_index for why re-deriving cannot be made safe).
|
|
876
|
+
names = symbol_index(idx["entries"], args.include_generic_expanded)
|
|
877
|
+
|
|
878
|
+
for ck, entries in sorted(by_conn.items()):
|
|
879
|
+
input_types = NamedTypeRegistry(
|
|
880
|
+
suffix="Input", all_optional=False, merge_reused=False, is_input=True,
|
|
881
|
+
)
|
|
882
|
+
output_types = NamedTypeRegistry()
|
|
883
|
+
loaded: list[tuple[dict, dict, dict[str, TypeNode], dict[str, TypeNode]]] = []
|
|
884
|
+
for e in sorted(entries, key=lambda x: x["nodeType"]):
|
|
885
|
+
entry = json.loads((lib / e["path"]).read_text())
|
|
886
|
+
v1def_raw = json.loads((lib / e["path"].replace(".json", ".v1def.json")).read_text())
|
|
887
|
+
v1def = v1def_raw[0] if isinstance(v1def_raw, list) else v1def_raw
|
|
888
|
+
is_event = is_event_node(e["nodeType"])
|
|
889
|
+
operation_input_fields = mark_lookup_fields(
|
|
890
|
+
entry, descriptor_input_fields(entry, v1def), invariant,
|
|
891
|
+
)
|
|
892
|
+
output_fields = (entry.get("outputSchema") or {}).get("fields") or []
|
|
893
|
+
input_tree = build_type_tree(operation_input_fields)
|
|
894
|
+
# The required-input gap is a statement about a connector ACTION's
|
|
895
|
+
# inputs; an event's parameters are optional by construction (an
|
|
896
|
+
# unscoped subscription is legal, `check` only warns).
|
|
897
|
+
gap = None if is_event else required_input_gap(entry, input_tree)
|
|
898
|
+
if gap:
|
|
899
|
+
required_input_gaps.append(gap)
|
|
900
|
+
output_tree = build_type_tree(output_fields)
|
|
901
|
+
input_types.discover(input_tree)
|
|
902
|
+
output_types.discover(output_tree)
|
|
903
|
+
loaded.append((entry, v1def, input_tree, output_tree))
|
|
904
|
+
|
|
905
|
+
operations: list[str] = []
|
|
906
|
+
for entry, v1def, input_tree, output_tree in loaded:
|
|
907
|
+
operations += emit_op(
|
|
908
|
+
entry, v1def, input_tree, output_tree, names[entry["nodeType"]],
|
|
909
|
+
input_types, output_types, invariant,
|
|
910
|
+
)
|
|
911
|
+
operations.append("")
|
|
912
|
+
|
|
913
|
+
emitted_types = input_types.emit()
|
|
914
|
+
rendered = "\n".join(emitted_types + operations)
|
|
915
|
+
specifiers = [s for s in ("descriptor", "triggerDescriptor") if f"= {s}<" in rendered] or ["descriptor"]
|
|
916
|
+
for alias in ("ConnectorLookupValue", "ConnectorValue"):
|
|
917
|
+
if alias in rendered:
|
|
918
|
+
specifiers.append(f"type {alias}")
|
|
919
|
+
body: list[str] = [
|
|
920
|
+
f"import {{ {', '.join(specifiers)} }} from {json.dumps(args.imp)};", "",
|
|
921
|
+
]
|
|
922
|
+
body += emitted_types
|
|
923
|
+
body += output_types.emit()
|
|
924
|
+
body += operations
|
|
925
|
+
(out_dir / f"{ck}.ts").write_text("\n".join(body).rstrip() + "\n")
|
|
926
|
+
written += 1
|
|
927
|
+
if required_input_gaps:
|
|
928
|
+
sys.exit(
|
|
929
|
+
"connector wrapper generation stopped:\n- "
|
|
930
|
+
+ "\n- ".join(required_input_gaps)
|
|
931
|
+
)
|
|
932
|
+
print(f"generated {written} connector module(s) → {out_dir}")
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
if __name__ == "__main__":
|
|
936
|
+
main()
|