@uipath/maestro-builder-sdk 6.3.0 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-index.md +6 -6
- package/dist/cli/commands/registry.js +68 -12
- package/dist/registry/archive/entry-policy.d.ts +36 -0
- package/dist/registry/archive/entry-policy.js +60 -0
- package/dist/registry/archive/extract.d.ts +31 -0
- package/dist/registry/archive/extract.js +92 -0
- package/dist/registry/archive/zip-reader.d.ts +38 -0
- package/dist/registry/archive/zip-reader.js +256 -0
- package/dist/registry/codecs/pyjson.d.ts +62 -0
- package/dist/registry/codecs/pyjson.js +208 -0
- package/dist/registry/compat/compare.d.ts +18 -0
- package/dist/registry/compat/compare.js +32 -0
- package/dist/registry/compat/paths.d.ts +4 -0
- package/dist/registry/compat/paths.js +17 -0
- package/dist/registry/compat/pystr.d.ts +50 -0
- package/dist/registry/compat/pystr.js +221 -0
- package/dist/registry/concurrency.d.ts +10 -0
- package/dist/registry/concurrency.js +34 -0
- package/dist/registry/curated-overlay.d.ts +36 -4
- package/dist/registry/curated-overlay.js +87 -14
- package/dist/registry/errors.d.ts +26 -0
- package/dist/registry/errors.js +38 -0
- package/dist/registry/extractor.d.ts +9 -10
- package/dist/registry/extractor.js +5 -15
- package/dist/registry/files.d.ts +26 -0
- package/dist/registry/files.js +160 -0
- package/dist/registry/generators/descriptors.d.ts +81 -0
- package/dist/registry/generators/descriptors.js +405 -0
- package/dist/registry/generators/fields.d.ts +46 -0
- package/dist/registry/generators/fields.js +111 -0
- package/dist/registry/generators/markdown/common.d.ts +21 -0
- package/dist/registry/generators/markdown/common.js +55 -0
- package/dist/registry/generators/markdown/index.d.ts +33 -0
- package/dist/registry/generators/markdown/index.js +37 -0
- package/dist/registry/generators/markdown/page.d.ts +8 -0
- package/dist/registry/generators/markdown/page.js +52 -0
- package/dist/registry/generators/markdown/pyvalue.d.ts +34 -0
- package/dist/registry/generators/markdown/pyvalue.js +106 -0
- package/dist/registry/generators/markdown/sections/choosing-object.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/choosing-object.js +52 -0
- package/dist/registry/generators/markdown/sections/example.d.ts +22 -0
- package/dist/registry/generators/markdown/sections/example.js +295 -0
- package/dist/registry/generators/markdown/sections/filtering.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/filtering.js +76 -0
- package/dist/registry/generators/markdown/sections/header.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/header.js +37 -0
- package/dist/registry/generators/markdown/sections/inputs.d.ts +30 -0
- package/dist/registry/generators/markdown/sections/inputs.js +151 -0
- package/dist/registry/generators/markdown/sections/lookups.d.ts +12 -0
- package/dist/registry/generators/markdown/sections/lookups.js +150 -0
- package/dist/registry/generators/markdown/sections/outputs.d.ts +3 -0
- package/dist/registry/generators/markdown/sections/outputs.js +9 -0
- package/dist/registry/generators/markdown/sections/resolving-fields.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/resolving-fields.js +139 -0
- package/dist/registry/generators/markdown/sections/schema-dynamic.d.ts +2 -0
- package/dist/registry/generators/markdown/sections/schema-dynamic.js +86 -0
- package/dist/registry/generators/materialize.d.ts +30 -0
- package/dist/registry/generators/materialize.js +132 -0
- package/dist/registry/generators/md-symbols.d.ts +22 -0
- package/dist/registry/generators/md-symbols.js +58 -0
- package/dist/registry/generators/triggers.d.ts +15 -0
- package/dist/registry/generators/triggers.js +85 -0
- package/dist/registry/generators/type-registry.d.ts +50 -0
- package/dist/registry/generators/type-registry.js +154 -0
- package/dist/registry/generators/type-tree.d.ts +65 -0
- package/dist/registry/generators/type-tree.js +179 -0
- package/dist/registry/library-build/build.d.ts +57 -0
- package/dist/registry/library-build/build.js +286 -0
- package/dist/registry/library-build/canonical.d.ts +119 -0
- package/dist/registry/library-build/canonical.js +512 -0
- package/dist/registry/library-build/cli.d.ts +45 -0
- package/dist/registry/library-build/cli.js +182 -0
- package/dist/registry/library-build/context.d.ts +107 -0
- package/dist/registry/library-build/context.js +267 -0
- package/dist/registry/library-build/enrich.d.ts +36 -0
- package/dist/registry/library-build/enrich.js +158 -0
- package/dist/registry/library-build/events.d.ts +108 -0
- package/dist/registry/library-build/events.js +476 -0
- package/dist/registry/library-build/generic-expand.d.ts +77 -0
- package/dist/registry/library-build/generic-expand.js +363 -0
- package/dist/registry/library-build/plan.d.ts +71 -0
- package/dist/registry/library-build/plan.js +270 -0
- package/dist/registry/library-build/py.d.ts +45 -0
- package/dist/registry/library-build/py.js +167 -0
- package/dist/registry/library-build/schema-action.d.ts +38 -0
- package/dist/registry/library-build/schema-action.js +107 -0
- package/dist/registry/library-build/write.d.ts +6 -0
- package/dist/registry/library-build/write.js +26 -0
- package/dist/registry/library-reader.d.ts +20 -0
- package/dist/registry/library-reader.js +50 -0
- package/dist/registry/lookup-specs.d.ts +55 -0
- package/dist/registry/lookup-specs.js +140 -0
- package/dist/registry/materialize-cli.js +28 -14
- package/dist/registry/materializer.d.ts +25 -3
- package/dist/registry/materializer.js +45 -29
- package/dist/registry/model/library.d.ts +254 -0
- package/dist/registry/model/library.js +23 -0
- package/dist/registry/naming.d.ts +54 -0
- package/dist/registry/naming.js +142 -0
- package/dist/registry/orphans.d.ts +4 -0
- package/dist/registry/orphans.js +86 -0
- package/dist/registry/prepare/actions.d.ts +26 -0
- package/dist/registry/prepare/actions.js +94 -0
- package/dist/registry/prepare/connections.d.ts +181 -0
- package/dist/registry/prepare/connections.js +399 -0
- package/dist/registry/prepare/event.d.ts +96 -0
- package/dist/registry/prepare/event.js +268 -0
- package/dist/registry/prepare/handoff.d.ts +21 -0
- package/dist/registry/prepare/handoff.js +91 -0
- package/dist/registry/prepare/ledger.d.ts +104 -0
- package/dist/registry/prepare/ledger.js +216 -0
- package/dist/registry/prepare/messages.d.ts +190 -0
- package/dist/registry/prepare/messages.js +472 -0
- package/dist/registry/prepare/objects.d.ts +130 -0
- package/dist/registry/prepare/objects.js +318 -0
- package/dist/registry/prepare/overlay.d.ts +149 -0
- package/dist/registry/prepare/overlay.js +487 -0
- package/dist/registry/prepare/parents.d.ts +41 -0
- package/dist/registry/prepare/parents.js +122 -0
- package/dist/registry/prepare/pipeline.d.ts +91 -0
- package/dist/registry/prepare/pipeline.js +376 -0
- package/dist/registry/prepare/regenerate.d.ts +9 -0
- package/dist/registry/prepare/regenerate.js +50 -0
- package/dist/registry/prepare/resolve.d.ts +199 -0
- package/dist/registry/prepare/resolve.js +470 -0
- package/dist/registry/prepare/retry.d.ts +83 -0
- package/dist/registry/prepare/retry.js +109 -0
- package/dist/registry/prepare/source-import.d.ts +54 -0
- package/dist/registry/prepare/source-import.js +192 -0
- package/dist/registry/prepare/support.d.ts +41 -0
- package/dist/registry/prepare/support.js +178 -0
- package/dist/registry/prepare-cli.d.ts +16 -5
- package/dist/registry/prepare-cli.js +60 -53
- package/dist/registry/pull-cli.js +8 -7
- package/dist/registry/reporter.d.ts +26 -0
- package/dist/registry/reporter.js +41 -0
- package/dist/registry/uip/client.d.ts +99 -0
- package/dist/registry/uip/client.js +226 -0
- package/dist/registry/uip/commands.d.ts +40 -0
- package/dist/registry/uip/commands.js +80 -0
- package/dist/registry/uip/envelope.d.ts +37 -0
- package/dist/registry/uip/envelope.js +84 -0
- package/dist/registry/uip/response-cache.d.ts +28 -0
- package/dist/registry/uip/response-cache.js +57 -0
- package/lib/README.md +10 -8
- package/package.json +3 -3
- package/dist/generators/_connections.py +0 -331
- package/dist/generators/_events.py +0 -355
- package/dist/generators/_lookups.py +0 -114
- package/dist/generators/_naming.py +0 -185
- package/dist/generators/_objects.py +0 -299
- package/dist/generators/_resolve.py +0 -319
- package/dist/generators/convert_library_to_md.py +0 -1441
- package/dist/generators/extract_library.py +0 -125
- package/dist/generators/generate_connectors_ts.py +0 -936
- package/dist/generators/generate_library_json.py +0 -2527
- package/dist/generators/generate_triggers_ts.py +0 -110
- package/dist/generators/materialize_registry.py +0 -155
- package/dist/generators/prepare_connector.py +0 -1860
- package/dist/registry/python.d.ts +0 -26
- package/dist/registry/python.js +0 -46
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filesystem moves the registry makes, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Generators return strings and maps; these helpers put them on disk. The
|
|
5
|
+
* stage-and-swap dance exists because two `registry pull`s can race on the
|
|
6
|
+
* shared cache when a coding agent issues tool calls in parallel, and a
|
|
7
|
+
* converter that wipes and rewrites its output directory in place left one of
|
|
8
|
+
* them mid-write.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { messageOf } from "./errors.js";
|
|
13
|
+
export function ensureDir(path) {
|
|
14
|
+
mkdirSync(path, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
export function writeText(path, text) {
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
18
|
+
writeFileSync(path, text, "utf8");
|
|
19
|
+
}
|
|
20
|
+
export function writeBytes(path, bytes) {
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
writeFileSync(path, bytes);
|
|
23
|
+
}
|
|
24
|
+
export function readText(path) {
|
|
25
|
+
return readFileSync(path, "utf8");
|
|
26
|
+
}
|
|
27
|
+
export function readJson(path) {
|
|
28
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
export function isFile(path) {
|
|
31
|
+
try {
|
|
32
|
+
return statSync(path).isFile();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function isDirectory(path) {
|
|
39
|
+
try {
|
|
40
|
+
return statSync(path).isDirectory();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Every file under `root`, as paths relative to it with `/` separators, sorted. */
|
|
47
|
+
export function listFiles(root, filter) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const walk = (dir, prefix) => {
|
|
50
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
52
|
+
if (entry.isDirectory())
|
|
53
|
+
walk(join(dir, entry.name), rel);
|
|
54
|
+
else if (entry.isFile() && (!filter || filter(rel)))
|
|
55
|
+
out.push(rel);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
if (existsSync(root))
|
|
59
|
+
walk(root, "");
|
|
60
|
+
return out.sort();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a directory beside its final name, then swap it into place.
|
|
64
|
+
*
|
|
65
|
+
* `fill` writes into `<target>.tmp-<pid>`. When it returns, any existing
|
|
66
|
+
* `target` is renamed to `<target>.old-<pid>`, the staging directory takes the
|
|
67
|
+
* final name, and the old tree is removed. A failure inside `fill` leaves the
|
|
68
|
+
* previous `target` untouched and cleans up the staging directory; a failure
|
|
69
|
+
* of the final rename puts the parked tree back, so the previous output
|
|
70
|
+
* survives as it did when Python's `Path.rename` raised.
|
|
71
|
+
*
|
|
72
|
+
* On overlayfs — every Docker RUN after the first — a directory written by an
|
|
73
|
+
* earlier layer cannot be renamed, so the parking step falls back to deleting
|
|
74
|
+
* it. See the comment on that branch for what the fallback gives up.
|
|
75
|
+
*/
|
|
76
|
+
export function stageAndSwap(target, fill) {
|
|
77
|
+
const staging = `${target}.tmp-${process.pid}`;
|
|
78
|
+
const previous = `${target}.old-${process.pid}`;
|
|
79
|
+
rmSync(staging, { recursive: true, force: true });
|
|
80
|
+
mkdirSync(staging, { recursive: true });
|
|
81
|
+
try {
|
|
82
|
+
fill(staging);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
rmSync(staging, { recursive: true, force: true });
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
89
|
+
// Which of the three states the target is in when the final rename runs, so
|
|
90
|
+
// a failure there can say honestly what is and is not still on disk.
|
|
91
|
+
let parked = "nothing-was-there";
|
|
92
|
+
if (existsSync(target)) {
|
|
93
|
+
try {
|
|
94
|
+
renameSync(target, previous);
|
|
95
|
+
parked = "parked";
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// EXDEV on a rename INSIDE one directory looks impossible, and on
|
|
99
|
+
// overlayfs it is routine: a directory that came from a lower layer
|
|
100
|
+
// cannot be renamed at all, because the kernel would have to copy the
|
|
101
|
+
// whole tree up first, so it answers "cross-device link not permitted".
|
|
102
|
+
// The e2e image hits this exactly — `registry pull` writes library-md in
|
|
103
|
+
// one RUN and `registry materialize` swaps it in the next.
|
|
104
|
+
//
|
|
105
|
+
// So: delete instead of parking. Be clear about what that costs. From
|
|
106
|
+
// here until the rename below succeeds, `target` does not exist, and if
|
|
107
|
+
// that rename fails there is no parked copy to put back — the PREVIOUS
|
|
108
|
+
// tree is gone for good. The NEW tree is not: it is complete in
|
|
109
|
+
// `staging`, which is left in place on that failure and named in the
|
|
110
|
+
// error, so an operator can finish the move by hand. The alternative
|
|
111
|
+
// that keeps the old tree, copying it aside first, costs a second pass
|
|
112
|
+
// over 7,000 files inside a Docker build where nothing races the swap;
|
|
113
|
+
// the window is accepted there and only there. The atomic path stays the
|
|
114
|
+
// default everywhere a rename works.
|
|
115
|
+
if (error?.code !== "EXDEV")
|
|
116
|
+
throw error;
|
|
117
|
+
rmSync(target, { recursive: true, force: true });
|
|
118
|
+
parked = "deleted";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
renameSync(staging, target);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// The new tree is deliberately NOT cleaned up here: it is the only copy
|
|
126
|
+
// when the overlayfs branch ran, and finishing the move by hand is the
|
|
127
|
+
// recovery. It is not ALWAYS there, though — the rename can fail because
|
|
128
|
+
// the staging tree itself went missing, which is what a second run sharing
|
|
129
|
+
// this output with the same pid does, the race the `.tmp-<pid>` naming
|
|
130
|
+
// already admits. Sending an operator after a directory that is not there
|
|
131
|
+
// costs them the time it takes to distrust the message.
|
|
132
|
+
let outcome;
|
|
133
|
+
if (parked === "parked") {
|
|
134
|
+
try {
|
|
135
|
+
renameSync(previous, target);
|
|
136
|
+
outcome = `the previous tree was put back at ${target}`;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
outcome = `the previous tree could not be put back and is still parked at ${previous}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (parked === "deleted") {
|
|
143
|
+
outcome = "the previous tree was already deleted by the overlayfs fallback and is gone";
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
outcome = "there was no previous tree";
|
|
147
|
+
}
|
|
148
|
+
const remains = existsSync(staging)
|
|
149
|
+
? `the complete new tree is at ${staging}`
|
|
150
|
+
: `the new tree is NOT at ${staging} either, so this output has to be built again`;
|
|
151
|
+
throw new Error(`stageAndSwap: could not move ${staging} to ${target}: ${messageOf(error)}; ${outcome}, and ${remains}`, { cause: error });
|
|
152
|
+
}
|
|
153
|
+
if (parked === "parked")
|
|
154
|
+
rmSync(previous, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
/** Remove a directory tree if it exists, then create it empty. */
|
|
157
|
+
export function resetDir(path) {
|
|
158
|
+
rmSync(path, { recursive: true, force: true });
|
|
159
|
+
mkdirSync(path, { recursive: true });
|
|
160
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { LoadedLibrary } from "../library-reader.js";
|
|
2
|
+
import { type LookupSpecData, type LookupStrategy } from "../lookup-specs.js";
|
|
3
|
+
import { type LibraryEntry, type LibraryField, type V1Definition } from "../model/library.js";
|
|
4
|
+
import { NamedTypeRegistry } from "./type-registry.js";
|
|
5
|
+
import { type TreeField, type TypeNode, type TypeTree } from "./type-tree.js";
|
|
6
|
+
/** A field name is a valid identifier only if it has no dots, brackets, etc.; otherwise it is quoted. */
|
|
7
|
+
export declare function keyLiteral(name: string): string;
|
|
8
|
+
/** One sentence for a lookup field's own doc comment: what it sends, how to get one. */
|
|
9
|
+
export declare function lookupFieldNote(declared: TreeField): string;
|
|
10
|
+
/**
|
|
11
|
+
* One property line.
|
|
12
|
+
*
|
|
13
|
+
* An INPUT property is wrapped so it also accepts an expression, and a
|
|
14
|
+
* resolvable lookup additionally accepts an unresolved token. The wrapper goes
|
|
15
|
+
* here rather than on the leaf type so that `tags: input('tags')` — the whole
|
|
16
|
+
* collection as one expression — is legal alongside `tags: ['a', out(…)]`.
|
|
17
|
+
* An OUTPUT property is never wrapped: it came back from the connector.
|
|
18
|
+
*/
|
|
19
|
+
export declare function emitNestedProperty(name: string, node: TypeNode, indent: string, namedTypes: NamedTypeRegistry, allOptional: boolean, isInput?: boolean): string[];
|
|
20
|
+
/** `export interface <name> { … }` over the roots of one tree. */
|
|
21
|
+
export declare function emitInterface(name: string, allOptional: boolean, namedTypes: NamedTypeRegistry, fieldTree: TypeTree, isInput?: boolean): string[];
|
|
22
|
+
export declare const STRATEGY_PROSE: Readonly<Record<LookupStrategy, string>>;
|
|
23
|
+
/**
|
|
24
|
+
* The grep-able header: the lookups stated in prose beside the data, for agents
|
|
25
|
+
* that read source with Grep and have no language server. Bare ` * ` body
|
|
26
|
+
* lines, to be spliced inside a `/** … *\/` block.
|
|
27
|
+
*/
|
|
28
|
+
export declare function emitLookupComment(specs: ReadonlyMap<string, LookupSpecData>, symbol?: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* The descriptor's `lookups:` literal — a RUNTIME value, not a type. `lookup()`
|
|
31
|
+
* reads `aliases` from it to build the `.byName()` family.
|
|
32
|
+
*/
|
|
33
|
+
export declare function emitLookupsBlock(specs: ReadonlyMap<string, LookupSpecData>): string[];
|
|
34
|
+
/**
|
|
35
|
+
* A connector EVENT as a `triggerDescriptor()`, the shape `onEvent()` takes.
|
|
36
|
+
*
|
|
37
|
+
* The payload type comes from `outputSchema`, and `where` from the entry's
|
|
38
|
+
* event parameters. An entry that declares none keeps a loose
|
|
39
|
+
* `Record<string, string>`: that is either an event which genuinely takes no
|
|
40
|
+
* parameters (HTTP Webhook) or one nothing has described yet, and an empty
|
|
41
|
+
* interface would refuse every key in both cases.
|
|
42
|
+
*/
|
|
43
|
+
export declare function emitEvent(entry: LibraryEntry, inFields: readonly LibraryField[], inputTree: TypeTree, outputTree: TypeTree, name: string, inputTypes: NamedTypeRegistry, outputTypes: NamedTypeRegistry, invariant?: ReadonlySet<string>): string[];
|
|
44
|
+
/** One operation: its `Inputs`/`Outputs` interfaces, doc comment and `descriptor()` const. */
|
|
45
|
+
export declare function emitOp(entry: LibraryEntry, v1def: V1Definition, inputTree: TypeTree, outputTree: TypeTree, name: string, inputTypes: NamedTypeRegistry, outputTypes: NamedTypeRegistry, invariant?: ReadonlySet<string>): string[];
|
|
46
|
+
/**
|
|
47
|
+
* The catalog names required inputs but the generated `Inputs` interface is
|
|
48
|
+
* empty: the descriptor would type-check a call the platform rejects. One line
|
|
49
|
+
* naming the operation and the fields, or `undefined` when there is no gap.
|
|
50
|
+
*/
|
|
51
|
+
export declare function requiredInputGap(entry: LibraryEntry, inputTree: TypeTree): string | undefined;
|
|
52
|
+
export interface DescriptorOptions {
|
|
53
|
+
/** Module specifier for `import { descriptor }`; {@link PACKAGE_NAME} by default. */
|
|
54
|
+
importSpecifier?: string;
|
|
55
|
+
/** Connector keys to emit; every connector when omitted or empty. */
|
|
56
|
+
connectors?: readonly string[];
|
|
57
|
+
/**
|
|
58
|
+
* Emit typed descriptors for `GenericExpanded` entries (and every per-object
|
|
59
|
+
* variant of a generic event). Off for the bulk library; on for the small
|
|
60
|
+
* connection-resolved overlays `prepare` writes.
|
|
61
|
+
*/
|
|
62
|
+
includeGenericExpanded?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface DescriptorOutput {
|
|
65
|
+
/** `"<connector-key>.ts"` → module text, in emit order (connector keys sorted by code point). */
|
|
66
|
+
modules: Map<string, string>;
|
|
67
|
+
/** nodeType → exported const name, from `naming.symbolIndex`, for the whole index. */
|
|
68
|
+
symbols: Map<string, string>;
|
|
69
|
+
/** One line per operation whose catalog requires inputs its interface cannot carry. */
|
|
70
|
+
requiredInputGaps: string[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Render every connector module of the library.
|
|
74
|
+
*
|
|
75
|
+
* The Python `main()` wrote every module and only then exited 1 on
|
|
76
|
+
* {@link DescriptorOutput.requiredInputGaps}; this returns both and the caller
|
|
77
|
+
* decides. A missing entry or sidecar throws, as the Python did.
|
|
78
|
+
*/
|
|
79
|
+
export declare function renderDescriptorModules(library: LoadedLibrary, options?: DescriptorOptions): DescriptorOutput;
|
|
80
|
+
/** The text the Python printed before exiting 1 on gaps; call only with a non-empty list. */
|
|
81
|
+
export declare function formatRequiredInputGaps(gaps: readonly string[]): string;
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statically-typed connector descriptor modules from the canonical JSON library.
|
|
3
|
+
*
|
|
4
|
+
* For every Curated and HTTP-request operation, one `descriptor<Inputs,
|
|
5
|
+
* Outputs>()` const plus its `Inputs`/`Outputs` interfaces; for every connector
|
|
6
|
+
* EVENT, a `triggerDescriptor<Where, Outputs>()` const. Grouped one module per
|
|
7
|
+
* connector, `connectors/<connector-key>.ts`. Authored flows import the exported
|
|
8
|
+
* consts by name, so the module text is an API contract: it is byte-for-byte
|
|
9
|
+
* what `generate_connectors_ts.py` wrote. The differential that proved that
|
|
10
|
+
* ran while the Python was in the tree and was deleted with it, so
|
|
11
|
+
* `tests/registry-descriptors.test.ts` is now the description of the text.
|
|
12
|
+
*
|
|
13
|
+
* Inputs are the union of the sidecar's body fields and the entry's query/path/
|
|
14
|
+
* file-multipart parameters (see `fields.ts`); dotted and wildcard names are
|
|
15
|
+
* rebuilt as nested objects and arrays (`type-tree.ts`) and named once per
|
|
16
|
+
* connector (`type-registry.ts`). Outputs come from `outputSchema.fields`.
|
|
17
|
+
* `GenericExpanded` operations are skipped unless asked for, and a generic
|
|
18
|
+
* event's per-object variants are skipped because they share their template's
|
|
19
|
+
* node type and so would emit the same const twice.
|
|
20
|
+
*/
|
|
21
|
+
import { PACKAGE_NAME } from "../../package-name.js";
|
|
22
|
+
import { pyJsonString, pyJsonStringify } from "../codecs/pyjson.js";
|
|
23
|
+
import { sortedByCodePoint, sortedStrings } from "../compat/compare.js";
|
|
24
|
+
import { afterLast, pyRstrip, pyStr, pyStrip, splitOnce } from "../compat/pystr.js";
|
|
25
|
+
import { referenceOf, specsFromFields } from "../lookup-specs.js";
|
|
26
|
+
import { isEventNodeType, } from "../model/library.js";
|
|
27
|
+
import { symbolIndex } from "../naming.js";
|
|
28
|
+
import { descriptorInputFields, lookupSpecs, markLookupFields } from "./fields.js";
|
|
29
|
+
import { NamedTypeRegistry } from "./type-registry.js";
|
|
30
|
+
import { buildTypeTree, pyTruthy, subtreeRequired } from "./type-tree.js";
|
|
31
|
+
/** `str(value or "")`. */
|
|
32
|
+
function strOrEmpty(value) {
|
|
33
|
+
return pyTruthy(value) ? pyStr(value) : "";
|
|
34
|
+
}
|
|
35
|
+
/** A field name is a valid identifier only if it has no dots, brackets, etc.; otherwise it is quoted. */
|
|
36
|
+
export function keyLiteral(name) {
|
|
37
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : pyJsonString(name);
|
|
38
|
+
}
|
|
39
|
+
/** One sentence for a lookup field's own doc comment: what it sends, how to get one. */
|
|
40
|
+
export function lookupFieldNote(declared) {
|
|
41
|
+
const reference = referenceOf(declared) ?? {};
|
|
42
|
+
const target = pyStr(pyTruthy(reference.objectName) ? reference.objectName : splitOnce(strOrEmpty(reference.path), "?")[0]);
|
|
43
|
+
const value = pyStr(pyTruthy(reference.lookupValue) ? reference.lookupValue : "id");
|
|
44
|
+
const field = strOrEmpty(declared.name);
|
|
45
|
+
return (`LOOKUP — sends a \`${target}.${value}\`, never a name or e-mail: ` +
|
|
46
|
+
`lookup(<Descriptor>, '${field}') (helpers in the LOOKUP lines above the descriptor).`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One property line.
|
|
50
|
+
*
|
|
51
|
+
* An INPUT property is wrapped so it also accepts an expression, and a
|
|
52
|
+
* resolvable lookup additionally accepts an unresolved token. The wrapper goes
|
|
53
|
+
* here rather than on the leaf type so that `tags: input('tags')` — the whole
|
|
54
|
+
* collection as one expression — is legal alongside `tags: ['a', out(…)]`.
|
|
55
|
+
* An OUTPUT property is never wrapped: it came back from the connector.
|
|
56
|
+
*/
|
|
57
|
+
export function emitNestedProperty(name, node, indent, namedTypes, allOptional, isInput = false) {
|
|
58
|
+
const lines = [];
|
|
59
|
+
const declared = node.declared ?? {};
|
|
60
|
+
let desc = pyStrip(strOrEmpty(declared.description)).replaceAll("\n", " ");
|
|
61
|
+
if (isInput && pyTruthy(declared.__lookup)) {
|
|
62
|
+
// The identity goes ON the field, not only in the LOOKUP block above the
|
|
63
|
+
// descriptor: "The reporter of the issue" beside `ConnectorLookupValue<string>`
|
|
64
|
+
// read as "any string", and an e-mail went into `fields.reporter.id`.
|
|
65
|
+
desc = pyStrip(`${desc} ${lookupFieldNote(declared)}`);
|
|
66
|
+
}
|
|
67
|
+
if (desc)
|
|
68
|
+
lines.push(`${indent}/** ${desc} */`);
|
|
69
|
+
const optional = allOptional || !subtreeRequired(node) ? "?" : "";
|
|
70
|
+
let rendered = namedTypes.typeFor(name, node);
|
|
71
|
+
if (isInput) {
|
|
72
|
+
const wrapper = pyTruthy(declared.__lookup) ? "ConnectorLookupValue" : "ConnectorValue";
|
|
73
|
+
rendered = `${wrapper}<${rendered}>`;
|
|
74
|
+
}
|
|
75
|
+
lines.push(`${indent}${keyLiteral(name)}${optional}: ${rendered};`);
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
/** `export interface <name> { … }` over the roots of one tree. */
|
|
79
|
+
export function emitInterface(name, allOptional, namedTypes, fieldTree, isInput = false) {
|
|
80
|
+
const out = [`export interface ${name} {`];
|
|
81
|
+
for (const [fieldName, node] of fieldTree) {
|
|
82
|
+
out.push(...emitNestedProperty(fieldName, node, " ", namedTypes, allOptional, isInput));
|
|
83
|
+
}
|
|
84
|
+
out.push("}");
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
export const STRATEGY_PROSE = {
|
|
88
|
+
filter: "one request (the operation declares a server-side filter)",
|
|
89
|
+
scan: "paged scan (this operation declares no filterPattern)",
|
|
90
|
+
tree: "hierarchical walk (the collection is a tree)",
|
|
91
|
+
dependent: "dependent — another field must resolve first",
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The grep-able header: the lookups stated in prose beside the data, for agents
|
|
95
|
+
* that read source with Grep and have no language server. Bare ` * ` body
|
|
96
|
+
* lines, to be spliced inside a `/** … *\/` block.
|
|
97
|
+
*/
|
|
98
|
+
export function emitLookupComment(specs, symbol = "<Descriptor>") {
|
|
99
|
+
const lines = [];
|
|
100
|
+
for (const [field, spec] of specs) {
|
|
101
|
+
const target = splitOnce(pyStr(pyTruthy(spec.objectName) ? spec.objectName : spec.path), "?")[0];
|
|
102
|
+
lines.push(` * LOOKUP: \`${field}\` -> ${target}.${spec.value}`);
|
|
103
|
+
const aliases = spec.aliases;
|
|
104
|
+
const offered = aliases.size > 0 ? [...aliases.values()].map((a) => `${a}()`) : [];
|
|
105
|
+
const byList = spec.by.map((n) => `\`${n}\``).join(", ");
|
|
106
|
+
lines.push(` * by: ${byList}`);
|
|
107
|
+
// THE WHOLE CALL, not just the method names: naming `.byEmail` without the
|
|
108
|
+
// `lookup(Descriptor, 'field')` wrapper says helpers exist but not how to
|
|
109
|
+
// reach one.
|
|
110
|
+
const first = aliases.size > 0 ? [...aliases.values()][0] : undefined;
|
|
111
|
+
const call = first
|
|
112
|
+
? `lookup(${symbol}, '${field}').${first}(value)`
|
|
113
|
+
: spec.by.length > 0
|
|
114
|
+
? `lookup(${symbol}, '${field}').by('${spec.by[0]}', value)`
|
|
115
|
+
: `lookup(${symbol}, '${field}')`;
|
|
116
|
+
lines.push(` * use: ${call}`);
|
|
117
|
+
if (offered.length > 0) {
|
|
118
|
+
lines.push(` * also: ${[...aliases.values()].map((a) => `.${a}`).join(", ")} · .by(name, value)`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
lines.push(" * also: .by(name, value) only — aliases collided");
|
|
122
|
+
}
|
|
123
|
+
lines.push(` * strategy: ${STRATEGY_PROSE[spec.strategy]}`);
|
|
124
|
+
if (spec.dependsOn && spec.dependsOn.length > 0) {
|
|
125
|
+
lines.push(` * resolve first: ${spec.dependsOn.map((d) => `\`${d}\``).join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The descriptor's `lookups:` literal — a RUNTIME value, not a type. `lookup()`
|
|
132
|
+
* reads `aliases` from it to build the `.byName()` family.
|
|
133
|
+
*/
|
|
134
|
+
export function emitLookupsBlock(specs) {
|
|
135
|
+
if (specs.size === 0)
|
|
136
|
+
return [];
|
|
137
|
+
const lines = [" lookups: {"];
|
|
138
|
+
for (const [field, spec] of specs) {
|
|
139
|
+
lines.push(` ${keyLiteral(field)}: {`);
|
|
140
|
+
if (pyTruthy(spec.objectName))
|
|
141
|
+
lines.push(` objectName: ${pyJsonString(spec.objectName)},`);
|
|
142
|
+
lines.push(` path: ${pyJsonStringify(spec.path)},`);
|
|
143
|
+
lines.push(` by: ${pyJsonStringify(spec.by)},`);
|
|
144
|
+
lines.push(` value: ${pyJsonString(spec.value)},`);
|
|
145
|
+
const aliases = [...spec.aliases].map(([name, alias]) => `${alias}: ${pyJsonString(name)}`).join(", ");
|
|
146
|
+
lines.push(` aliases: {${aliases ? ` ${aliases} ` : ""}},`);
|
|
147
|
+
lines.push(` strategy: ${pyJsonString(spec.strategy)},`);
|
|
148
|
+
if (spec.dependsOn && spec.dependsOn.length > 0)
|
|
149
|
+
lines.push(` dependsOn: ${pyJsonStringify(spec.dependsOn)},`);
|
|
150
|
+
lines.push(" },");
|
|
151
|
+
}
|
|
152
|
+
lines.push(" },");
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
/** `entry.get("version", "1.0.0")`: the key's value when present, even `null`. */
|
|
156
|
+
function versionOf(entry) {
|
|
157
|
+
return "version" in entry ? entry.version : "1.0.0";
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A connector EVENT as a `triggerDescriptor()`, the shape `onEvent()` takes.
|
|
161
|
+
*
|
|
162
|
+
* The payload type comes from `outputSchema`, and `where` from the entry's
|
|
163
|
+
* event parameters. An entry that declares none keeps a loose
|
|
164
|
+
* `Record<string, string>`: that is either an event which genuinely takes no
|
|
165
|
+
* parameters (HTTP Webhook) or one nothing has described yet, and an empty
|
|
166
|
+
* interface would refuse every key in both cases.
|
|
167
|
+
*/
|
|
168
|
+
export function emitEvent(entry, inFields, inputTree, outputTree, name, inputTypes, outputTypes, invariant = new Set()) {
|
|
169
|
+
const nodeType = entry.nodeType;
|
|
170
|
+
const eventId = afterLast(nodeType, ".");
|
|
171
|
+
const connectorKey = pyTruthy(entry.connector?.key) ? entry.connector.key : "";
|
|
172
|
+
const label = pyTruthy(entry.display?.label) ? entry.display.label : eventId;
|
|
173
|
+
const version = versionOf(entry);
|
|
174
|
+
const generic = entry.operation?.activityType === "GenericTrigger";
|
|
175
|
+
const lines = [];
|
|
176
|
+
let whereType = "Record<string, string>";
|
|
177
|
+
if (inFields.length > 0) {
|
|
178
|
+
whereType = `${name}Where`;
|
|
179
|
+
lines.push(...emitInterface(whereType, false, inputTypes, inputTree, true));
|
|
180
|
+
lines.push("");
|
|
181
|
+
}
|
|
182
|
+
lines.push(...emitInterface(`${name}Outputs`, true, outputTypes, outputTree));
|
|
183
|
+
lines.push("");
|
|
184
|
+
// A subscription's `where` values are resolved by `lookup()` exactly as a
|
|
185
|
+
// connector input's are, so the descriptor carries the same specs.
|
|
186
|
+
const specs = specsFromFields(inFields, invariant);
|
|
187
|
+
lines.push("/**");
|
|
188
|
+
lines.push(` * ${pyStr(label)} — ${nodeType}`);
|
|
189
|
+
lines.push(" *");
|
|
190
|
+
if (generic) {
|
|
191
|
+
lines.push(" * A GENERIC event: it fires for ONE object of the connection (an entity, a");
|
|
192
|
+
lines.push(` * table, a custom object), so name it — \`onEvent(${name}, { object: '<Object>' })\`.`);
|
|
193
|
+
lines.push(` * \`uip is triggers objects ${pyStr(connectorKey)} <EVENT> --connection-id <id>\` lists them.`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
lines.push(` * Start a flow on it with \`onEvent(${name}, { where: { … } })\`, or pause for it`);
|
|
197
|
+
lines.push(` * with \`waitForEvent(${name}, { … })\`; \`where\` carries its event parameters.`);
|
|
198
|
+
}
|
|
199
|
+
// INSIDE the block: `emitLookupComment` returns bare ` * ` body lines.
|
|
200
|
+
if (specs.size > 0) {
|
|
201
|
+
lines.push(" *");
|
|
202
|
+
lines.push(...emitLookupComment(specs, name));
|
|
203
|
+
}
|
|
204
|
+
lines.push(" */");
|
|
205
|
+
lines.push(`export const ${name} = triggerDescriptor<${whereType}, ${name}Outputs>()({`);
|
|
206
|
+
lines.push(` connector: ${pyJsonStringify(connectorKey)},`);
|
|
207
|
+
lines.push(` event: ${pyJsonString(eventId)},`);
|
|
208
|
+
lines.push(` version: ${pyJsonStringify(version)},`);
|
|
209
|
+
lines.push(` displayName: ${pyJsonStringify(label)},`);
|
|
210
|
+
lines.push(...emitLookupsBlock(specs));
|
|
211
|
+
lines.push("} as const);");
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
214
|
+
/** One operation: its `Inputs`/`Outputs` interfaces, doc comment and `descriptor()` const. */
|
|
215
|
+
export function emitOp(entry, v1def, inputTree, outputTree, name, inputTypes, outputTypes, invariant = new Set()) {
|
|
216
|
+
const nodeType = entry.nodeType;
|
|
217
|
+
const actionId = afterLast(nodeType, ".");
|
|
218
|
+
const operation = entry.operation ?? {};
|
|
219
|
+
const isGenericExpanded = operation.activityType === "GenericExpanded";
|
|
220
|
+
const label = pyTruthy(entry.display?.label) ? entry.display.label : actionId;
|
|
221
|
+
const version = versionOf(entry);
|
|
222
|
+
const runtime = entry.runtime ?? {};
|
|
223
|
+
const inFields = descriptorInputFields(entry, v1def);
|
|
224
|
+
if (isEventNodeType(nodeType)) {
|
|
225
|
+
return emitEvent(entry, inFields, inputTree, outputTree, name, inputTypes, outputTypes, invariant);
|
|
226
|
+
}
|
|
227
|
+
const lines = [];
|
|
228
|
+
lines.push(...emitInterface(`${name}Inputs`, false, inputTypes, inputTree, true));
|
|
229
|
+
lines.push("");
|
|
230
|
+
lines.push(...emitInterface(`${name}Outputs`, true, outputTypes, outputTree));
|
|
231
|
+
lines.push("");
|
|
232
|
+
// An operation whose real field set arrives at design time has an `Inputs`
|
|
233
|
+
// type that is either empty (no object chosen yet) or just the parents. The
|
|
234
|
+
// doc comment is the only place the typed surface can say so.
|
|
235
|
+
const schemaAction = operation.schemaAction ?? {};
|
|
236
|
+
const specs = lookupSpecs(entry, v1def, invariant);
|
|
237
|
+
const lookupComment = specs.size > 0 ? emitLookupComment(specs, name) : [];
|
|
238
|
+
const header = ` * ${pyStr(label)} — ${nodeType}`;
|
|
239
|
+
if (pyTruthy(operation.objectFromDiscovery)) {
|
|
240
|
+
lines.push("/**");
|
|
241
|
+
lines.push(header);
|
|
242
|
+
lines.push(" *");
|
|
243
|
+
lines.push(" * The object is chosen against the connection, so `Inputs` is empty");
|
|
244
|
+
lines.push(" * here. List the objects and read the chosen one's fields with");
|
|
245
|
+
lines.push(" * `uip is resources list` / `describe` before binding this node.");
|
|
246
|
+
if (lookupComment.length > 0) {
|
|
247
|
+
lines.push(" *");
|
|
248
|
+
lines.push(...lookupComment);
|
|
249
|
+
}
|
|
250
|
+
lines.push(" */");
|
|
251
|
+
}
|
|
252
|
+
else if (pyTruthy(schemaAction.parents)) {
|
|
253
|
+
const parents = schemaAction.parents.map((x) => `\`${pyStr(x)}\``).join(", ");
|
|
254
|
+
lines.push("/**");
|
|
255
|
+
lines.push(header);
|
|
256
|
+
lines.push(" *");
|
|
257
|
+
lines.push(` * \`Inputs\` carries only the fields that SELECT the schema (${parents}).`);
|
|
258
|
+
lines.push(` * The real set comes from the \`${pyStr(schemaAction.name)}\` design-time`);
|
|
259
|
+
lines.push(" * action — describe with those values supplied to see it.");
|
|
260
|
+
if (lookupComment.length > 0) {
|
|
261
|
+
lines.push(" *");
|
|
262
|
+
lines.push(...lookupComment);
|
|
263
|
+
}
|
|
264
|
+
lines.push(" */");
|
|
265
|
+
}
|
|
266
|
+
else if (lookupComment.length > 0) {
|
|
267
|
+
lines.push("/**");
|
|
268
|
+
lines.push(header);
|
|
269
|
+
lines.push(" *");
|
|
270
|
+
lines.push(...lookupComment);
|
|
271
|
+
lines.push(" */");
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
lines.push(`/** ${pyStr(label)} — ${nodeType} */`);
|
|
275
|
+
}
|
|
276
|
+
lines.push(`export const ${name} = descriptor<${name}Inputs, ${name}Outputs>()({`);
|
|
277
|
+
lines.push(` nodeType: ${pyJsonString(nodeType)},`);
|
|
278
|
+
lines.push(` version: ${pyJsonStringify(version)},`);
|
|
279
|
+
if (isGenericExpanded && pyTruthy(operation.objectName)) {
|
|
280
|
+
lines.push(` objectName: ${pyJsonStringify(operation.objectName)},`);
|
|
281
|
+
}
|
|
282
|
+
if (pyTruthy(runtime.requiresConnection))
|
|
283
|
+
lines.push(" requiresConnection: true,");
|
|
284
|
+
if (pyTruthy(runtime.requiresFolderKey))
|
|
285
|
+
lines.push(" requiresFolderKey: true,");
|
|
286
|
+
lines.push(...emitLookupsBlock(specs));
|
|
287
|
+
lines.push("} as const);");
|
|
288
|
+
return lines;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The catalog names required inputs but the generated `Inputs` interface is
|
|
292
|
+
* empty: the descriptor would type-check a call the platform rejects. One line
|
|
293
|
+
* naming the operation and the fields, or `undefined` when there is no gap.
|
|
294
|
+
*/
|
|
295
|
+
export function requiredInputGap(entry, inputTree) {
|
|
296
|
+
const required = (entry.inputSchema?.fields || [])
|
|
297
|
+
.filter((field) => field.required === true)
|
|
298
|
+
.map((field) => pyStr(pyTruthy(field.name) ? field.name : "<unnamed>"));
|
|
299
|
+
if (required.length === 0 || inputTree.size > 0)
|
|
300
|
+
return undefined;
|
|
301
|
+
const nodeType = "nodeType" in entry ? pyStr(entry.nodeType) : "<unknown node>";
|
|
302
|
+
return `${nodeType}: catalog requires ${required.join(", ")}, but the generated inputs interface is empty`;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* `mark_lookup_fields`: flag the fields whose TypeScript type must also accept
|
|
306
|
+
* a `LookupToken` — exactly the set `lookupSpecs` generates helpers for. The
|
|
307
|
+
* work is `fields.ts`'s `markLookupFields`, which also merges the catalog's
|
|
308
|
+
* `reference` INTO a field whose own declaration lacks one, as the Python did;
|
|
309
|
+
* `lookupFieldNote` reads the lookup's target and value from that merged
|
|
310
|
+
* reference, so the doc comment on the field depends on the merge. This
|
|
311
|
+
* wrapper only narrows the result to the tree's field type.
|
|
312
|
+
*/
|
|
313
|
+
function markedInputFields(entry, fields, invariant) {
|
|
314
|
+
return markLookupFields(entry, fields, invariant);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Render every connector module of the library.
|
|
318
|
+
*
|
|
319
|
+
* The Python `main()` wrote every module and only then exited 1 on
|
|
320
|
+
* {@link DescriptorOutput.requiredInputGaps}; this returns both and the caller
|
|
321
|
+
* decides. A missing entry or sidecar throws, as the Python did.
|
|
322
|
+
*/
|
|
323
|
+
export function renderDescriptorModules(library, options = {}) {
|
|
324
|
+
const importSpecifier = options.importSpecifier ?? PACKAGE_NAME;
|
|
325
|
+
const includeGenericExpanded = options.includeGenericExpanded ?? false;
|
|
326
|
+
const subset = new Set((options.connectors ?? []).filter((c) => c.length > 0));
|
|
327
|
+
const invariant = library.invariant;
|
|
328
|
+
const rows = library.index.entries;
|
|
329
|
+
// Group Curated + http-request entries by connector key.
|
|
330
|
+
const byConnector = new Map();
|
|
331
|
+
for (const row of rows) {
|
|
332
|
+
if (row.activityType === "GenericExpanded" && !includeGenericExpanded)
|
|
333
|
+
continue;
|
|
334
|
+
// A generic EVENT's per-object variants share their template's nodeType and
|
|
335
|
+
// so would emit the same const twice. Only the template gets a descriptor:
|
|
336
|
+
// `onEvent(RecordCreated, { object: 'Message' })` names the object as an
|
|
337
|
+
// option, so one descriptor serves every object.
|
|
338
|
+
if (row.activityType === "GenericTrigger" && pyTruthy(row.objectName) && !includeGenericExpanded)
|
|
339
|
+
continue;
|
|
340
|
+
const key = row.connectorKey;
|
|
341
|
+
if (subset.size > 0 && !subset.has(key))
|
|
342
|
+
continue;
|
|
343
|
+
const group = byConnector.get(key);
|
|
344
|
+
if (group)
|
|
345
|
+
group.push(row);
|
|
346
|
+
else
|
|
347
|
+
byConnector.set(key, [row]);
|
|
348
|
+
}
|
|
349
|
+
// ONE symbol map for the whole run, shared with the Markdown generator.
|
|
350
|
+
const symbols = symbolIndex(rows, includeGenericExpanded);
|
|
351
|
+
const modules = new Map();
|
|
352
|
+
const requiredInputGaps = [];
|
|
353
|
+
for (const key of sortedStrings(byConnector.keys())) {
|
|
354
|
+
const inputTypes = new NamedTypeRegistry({ suffix: "Input", allOptional: false, mergeReused: false, isInput: true });
|
|
355
|
+
const outputTypes = new NamedTypeRegistry();
|
|
356
|
+
const loaded = [];
|
|
357
|
+
for (const row of sortedByCodePoint(byConnector.get(key), (r) => r.nodeType)) {
|
|
358
|
+
const entry = library.readEntry(row);
|
|
359
|
+
if (entry === undefined)
|
|
360
|
+
throw new Error(`library entry missing: ${library.entryPath(row)}`);
|
|
361
|
+
const v1def = library.readV1Def(row);
|
|
362
|
+
if (v1def === undefined)
|
|
363
|
+
throw new Error(`library sidecar missing: ${library.v1defPath(row)}`);
|
|
364
|
+
const isEvent = isEventNodeType(row.nodeType);
|
|
365
|
+
const operationInputFields = markedInputFields(entry, descriptorInputFields(entry, v1def), invariant);
|
|
366
|
+
const outputFields = entry.outputSchema?.fields || [];
|
|
367
|
+
const inputTree = buildTypeTree(operationInputFields);
|
|
368
|
+
// The required-input gap is a statement about a connector ACTION's
|
|
369
|
+
// inputs; an event's parameters are optional by construction.
|
|
370
|
+
const gap = isEvent ? undefined : requiredInputGap(entry, inputTree);
|
|
371
|
+
if (gap)
|
|
372
|
+
requiredInputGaps.push(gap);
|
|
373
|
+
const outputTree = buildTypeTree(outputFields);
|
|
374
|
+
inputTypes.discover(inputTree);
|
|
375
|
+
outputTypes.discover(outputTree);
|
|
376
|
+
loaded.push({ entry, v1def, inputTree, outputTree });
|
|
377
|
+
}
|
|
378
|
+
const operations = [];
|
|
379
|
+
for (const { entry, v1def, inputTree, outputTree } of loaded) {
|
|
380
|
+
const name = symbols.get(entry.nodeType);
|
|
381
|
+
if (name === undefined)
|
|
382
|
+
throw new Error(`no symbol for ${entry.nodeType}: the entry's nodeType is not in index.json`);
|
|
383
|
+
operations.push(...emitOp(entry, v1def, inputTree, outputTree, name, inputTypes, outputTypes, invariant), "");
|
|
384
|
+
}
|
|
385
|
+
const emittedTypes = inputTypes.emit(emitNestedProperty);
|
|
386
|
+
const rendered = [...emittedTypes, ...operations].join("\n");
|
|
387
|
+
let specifiers = ["descriptor", "triggerDescriptor"].filter((s) => rendered.includes(`= ${s}<`));
|
|
388
|
+
if (specifiers.length === 0)
|
|
389
|
+
specifiers = ["descriptor"];
|
|
390
|
+
for (const alias of ["ConnectorLookupValue", "ConnectorValue"]) {
|
|
391
|
+
if (rendered.includes(alias))
|
|
392
|
+
specifiers.push(`type ${alias}`);
|
|
393
|
+
}
|
|
394
|
+
const body = [`import { ${specifiers.join(", ")} } from ${pyJsonString(importSpecifier)};`, ""];
|
|
395
|
+
body.push(...emittedTypes);
|
|
396
|
+
body.push(...outputTypes.emit(emitNestedProperty));
|
|
397
|
+
body.push(...operations);
|
|
398
|
+
modules.set(`${key}.ts`, `${pyRstrip(body.join("\n"))}\n`);
|
|
399
|
+
}
|
|
400
|
+
return { modules, symbols, requiredInputGaps };
|
|
401
|
+
}
|
|
402
|
+
/** The text the Python printed before exiting 1 on gaps; call only with a non-empty list. */
|
|
403
|
+
export function formatRequiredInputGaps(gaps) {
|
|
404
|
+
return `connector wrapper generation stopped:\n- ${gaps.join("\n- ")}`;
|
|
405
|
+
}
|