@uipath/maestro-builder-sdk 6.4.0 → 6.5.1
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/bpmn/uipath-moddle.v1.json +21 -0
- 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,181 @@
|
|
|
1
|
+
import { Refusal } from "../errors.js";
|
|
2
|
+
import type { UipClient } from "../uip/client.js";
|
|
3
|
+
/** One row of `uip is connections list`: `Id`, `Name`, `ConnectorKey`, `State`, `Folder`, `FolderKey`, … */
|
|
4
|
+
export type ConnectionRow = Record<string, unknown>;
|
|
5
|
+
/** Which listing the candidates came from, as the refusals name it. */
|
|
6
|
+
export type ConnectionScope = "your folder" | "the tenant";
|
|
7
|
+
/** No usable connection, with the reason and the way forward (`_connections.ConnectionError_`). */
|
|
8
|
+
export declare class ConnectionRefusal extends Refusal {
|
|
9
|
+
constructor(message: string, code: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `uip is connections list --output json`, as rows.
|
|
13
|
+
*
|
|
14
|
+
* A non-zero exit and a non-JSON reply are both refusals, never an empty list:
|
|
15
|
+
* "you have no connection" and "the listing failed" have opposite remedies.
|
|
16
|
+
*/
|
|
17
|
+
export declare function listConnections(client: UipClient, options: {
|
|
18
|
+
allFolders: boolean;
|
|
19
|
+
byoa?: boolean;
|
|
20
|
+
}): Promise<ConnectionRow[]>;
|
|
21
|
+
/**
|
|
22
|
+
* The rows for `connectorKey` that can serve a request, optionally by `Name`.
|
|
23
|
+
*
|
|
24
|
+
* A Failed connection is listed like any other and cannot serve a request;
|
|
25
|
+
* selecting one produces an error about the operation rather than about the
|
|
26
|
+
* connection, which sends the reader the wrong way.
|
|
27
|
+
*/
|
|
28
|
+
export declare function enabledConnections(rows: readonly ConnectionRow[], connectorKey: string, name?: string): ConnectionRow[];
|
|
29
|
+
/**
|
|
30
|
+
* The candidates, one pasteable `--connection-id` line each
|
|
31
|
+
* (`_connections.candidate_listing`): `<indent><Name> --connection-id <Id> (folder: <Folder>)`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function candidateListing(rows: readonly ConnectionRow[], indent?: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* The one enabled connection for `connectorKey` among `rows`, or throw.
|
|
36
|
+
*
|
|
37
|
+
* Pure: `rows` is whatever listing the caller fetched, and `scope` is how the
|
|
38
|
+
* refusal names it. Several candidates is a refusal that lists every one with
|
|
39
|
+
* its id — a NAME is not a key on a tenant. The eval tenant holds three Slack
|
|
40
|
+
* connections all named `is-sandboxes` (two Enabled, in different folders), so
|
|
41
|
+
* the old remedy — "name the one you want with --connection <name>" — handed
|
|
42
|
+
* back the very flag the caller had just used, and the doc-following author
|
|
43
|
+
* dead-ended (placeholder GUIDs hand-written into bindings.json, 401 at the
|
|
44
|
+
* Slack node).
|
|
45
|
+
*/
|
|
46
|
+
export declare function selectConnection(rows: readonly ConnectionRow[], connectorKey: string, name?: string, scope?: ConnectionScope): ConnectionRow;
|
|
47
|
+
/**
|
|
48
|
+
* The one enabled connection for `connectorKey`, fetched, or throw.
|
|
49
|
+
*
|
|
50
|
+
* Tries the DEFAULT listing first and only widens to `--all-folders` if that
|
|
51
|
+
* finds nothing. That order matters and is not just politeness to the API: the
|
|
52
|
+
* default listing is the caller's own folder, which on a real tenant is one
|
|
53
|
+
* connection where the tenant-wide view is many. Widening first would turn an
|
|
54
|
+
* unambiguous case into an ambiguous one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function discoverConnection(client: UipClient, connectorKey: string, name?: string): Promise<ConnectionRow>;
|
|
57
|
+
/**
|
|
58
|
+
* Every enabled connection for `connectorKey`, own folder FIRST
|
|
59
|
+
* (`_connections.candidate_connections`).
|
|
60
|
+
*
|
|
61
|
+
* {@link discoverConnection} answers "which one" and stops at the first
|
|
62
|
+
* non-empty scope, which is right for picking a connection and wrong for
|
|
63
|
+
* retrying one: when the own-folder listing holds exactly one, the tenant-wide
|
|
64
|
+
* connections are never even looked at. So a lookup that exhausts the
|
|
65
|
+
* personal-workspace connection reports a bad VALUE while the record sits in
|
|
66
|
+
* another folder's connection (flow-builder-sdk#744, measured on two Jira
|
|
67
|
+
* connections where only the second carried project `TS`).
|
|
68
|
+
*
|
|
69
|
+
* Ordering is the same preference discovery encodes — the caller's own folder
|
|
70
|
+
* is the likeliest and is tried first — but nothing is dropped. Deduplicated
|
|
71
|
+
* by `Id`, because the tenant-wide listing repeats the own-folder rows; a row
|
|
72
|
+
* with no `Id` is never deduplicated, as the Python's `if identifier and …`
|
|
73
|
+
* did not.
|
|
74
|
+
*
|
|
75
|
+
* This does NOT decide anything, and in particular does not throw on several
|
|
76
|
+
* matches: choosing among them is the caller's job, and the only honest way to
|
|
77
|
+
* choose is evidence (a lookup that resolves on exactly one). That keeps this
|
|
78
|
+
* module's rule intact — ambiguity is never resolved by guessing.
|
|
79
|
+
*/
|
|
80
|
+
export declare function candidateConnections(client: UipClient, connectorKey: string, name?: string): Promise<ConnectionRow[]>;
|
|
81
|
+
/** The enabled connections for `connectorKey`, one per line, pasteable. */
|
|
82
|
+
export declare function enabledListing(rows: readonly ConnectionRow[], connectorKey: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* The record for an explicitly named connection among `rows`, or throw unless it can serve.
|
|
85
|
+
*
|
|
86
|
+
* An explicit `--connection-id` used to skip every check discovery applies, and
|
|
87
|
+
* that skip is where a whole class of silent failures came from. A connection
|
|
88
|
+
* in `State: Failed` is listed like any other, and `uip is resources describe`
|
|
89
|
+
* against it answers `Result: Success` with NO request fields — the tenant has
|
|
90
|
+
* no schema to give for a connection it cannot reach. `prepare` then wrote an
|
|
91
|
+
* overlay with zero body inputs, printed "0 input field(s)" as a success, and
|
|
92
|
+
* the next `compile` rejected every real field as unknown. Measured on the eval
|
|
93
|
+
* tenant (Salesforce `Opportunity` / `Update`): the Failed connection returned
|
|
94
|
+
* 0 request fields, the Enabled one 13.
|
|
95
|
+
*
|
|
96
|
+
* So: resolve the id against the listing, and refuse — naming the state found
|
|
97
|
+
* and the enabled alternatives — rather than describe through it.
|
|
98
|
+
*/
|
|
99
|
+
export declare function checkConnection(rows: readonly ConnectionRow[], connectorKey: string, connectionId: string): ConnectionRow;
|
|
100
|
+
/** {@link checkConnection} against the tenant-wide listing — the id may live in another folder. */
|
|
101
|
+
export declare function lookupConnection(client: UipClient, connectorKey: string, connectionId: string): Promise<ConnectionRow>;
|
|
102
|
+
/**
|
|
103
|
+
* The enabled BYOA connections for `connectorKey`.
|
|
104
|
+
*
|
|
105
|
+
* THROWS on a failed listing rather than answering an empty list. The
|
|
106
|
+
* difference is the whole point: "you have no BYOA connection" tells the caller
|
|
107
|
+
* to go create one, and a listing that failed says nothing of the kind.
|
|
108
|
+
* Answering [] for both turns a broken call into confident, wrong advice.
|
|
109
|
+
*/
|
|
110
|
+
export declare function byoaConnections(client: UipClient, connectorKey: string): Promise<ConnectionRow[]>;
|
|
111
|
+
/** The BYOA candidates, one pasteable line each, or why there are none. */
|
|
112
|
+
export declare function byoaListing(rows: readonly ConnectionRow[], connectorKey: string): string;
|
|
113
|
+
/** `bindings.json` as this module reads and writes it; rows are the author's and stay loose. */
|
|
114
|
+
export type BindingsJson = Record<string, unknown> & {
|
|
115
|
+
schemaVersion?: unknown;
|
|
116
|
+
bindings: unknown[];
|
|
117
|
+
};
|
|
118
|
+
export interface BindingsMerge {
|
|
119
|
+
document: BindingsJson;
|
|
120
|
+
/** True when the connection row's `resourceKey` changed (or the row was added). */
|
|
121
|
+
wroteConnection: boolean;
|
|
122
|
+
/** True when the folder row's `resourceKey` changed (or the row was added). */
|
|
123
|
+
wroteFolder: boolean;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Add connection + folder entries to a parsed `bindings.json`, without touching the input.
|
|
127
|
+
*
|
|
128
|
+
* ADDITIVE AND IDEMPOTENT. An existing entry under the same symbolic name (its
|
|
129
|
+
* `name` or `id`) is updated, never duplicated, and every other entry is left
|
|
130
|
+
* exactly as it was — `bindings.json` is an authored file that may already
|
|
131
|
+
* carry bindings for connectors this run knows nothing about. An updated row
|
|
132
|
+
* keeps its key order and gains any missing key at the end; a new row is
|
|
133
|
+
* written `id, name, resource, resourceKey, default, propertyAttribute`.
|
|
134
|
+
*
|
|
135
|
+
* `document` is what the file parsed to, or `undefined` when there was no file.
|
|
136
|
+
* As in the Python, a parsed value that is not an object is replaced by the
|
|
137
|
+
* default document.
|
|
138
|
+
*/
|
|
139
|
+
export declare function mergeBindingsDocument(document: unknown, symbolicConnection: string, symbolicFolder: string, connection: ConnectionRow): BindingsMerge;
|
|
140
|
+
/**
|
|
141
|
+
* `json.dump(document, indent=2)` + newline: the bytes `bindings.json` is written in.
|
|
142
|
+
*
|
|
143
|
+
* `allowIntegerKeys` for the same reason the ledger passes it (`objects.ts`):
|
|
144
|
+
* `bindings.json` is the author's file and may carry an integer-like key
|
|
145
|
+
* anywhere in a row this module never looks at. `json.dump` wrote such a file
|
|
146
|
+
* back; refusing it here left the file untouched and the connection unbound,
|
|
147
|
+
* with an error about a key the author did not know they had. The one thing
|
|
148
|
+
* that cannot be kept is that key's position — `JSON.parse` has already
|
|
149
|
+
* hoisted it to the front of its object — which Python preserved.
|
|
150
|
+
*/
|
|
151
|
+
export declare function formatBindings(document: BindingsJson): string;
|
|
152
|
+
/**
|
|
153
|
+
* Add connection + folder entries to `bindings.json` at `path`, in place.
|
|
154
|
+
*
|
|
155
|
+
* Returns `[wroteConnection, wroteFolder]` describing what changed, so the
|
|
156
|
+
* caller can report the specific thing rather than "updated bindings". The file
|
|
157
|
+
* is always rewritten, as the Python did. A file that will not parse is REFUSED,
|
|
158
|
+
* not overwritten: it is the author's, and not ours to discard.
|
|
159
|
+
*/
|
|
160
|
+
export declare function mergeBindings(path: string, symbolicConnection: string, symbolicFolder: string, connection: ConnectionRow): [wroteConnection: boolean, wroteFolder: boolean];
|
|
161
|
+
/**
|
|
162
|
+
* Remember `bindings.json` byte-for-byte, before a stage that writes it.
|
|
163
|
+
*
|
|
164
|
+
* `undefined` means the file could not be read — normally that it did not
|
|
165
|
+
* exist — and {@link restoreBindings} will remove whatever was written since.
|
|
166
|
+
*/
|
|
167
|
+
export declare function snapshotBindings(path: string | undefined): Uint8Array | undefined;
|
|
168
|
+
/**
|
|
169
|
+
* Put `bindings.json` back exactly as {@link snapshotBindings} found it.
|
|
170
|
+
*
|
|
171
|
+
* A refusal must leave `bindings.json` as it was: the BYOA fact needs a
|
|
172
|
+
* connection to discover, so stage 0 has already written one by the time the
|
|
173
|
+
* event says it is the wrong KIND — without the rollback the overlay keeps
|
|
174
|
+
* pointing at a connection the tool just forbade. A failure to restore is
|
|
175
|
+
* reported through `warn` (stderr by default), never thrown: the refusal being
|
|
176
|
+
* delivered is the message that matters.
|
|
177
|
+
*
|
|
178
|
+
* `shown` is the path as the caller typed it (`args.bindings`), which is what
|
|
179
|
+
* the Python printed; `path` is where the file actually is.
|
|
180
|
+
*/
|
|
181
|
+
export declare function restoreBindings(path: string | undefined, snapshot: Uint8Array | undefined, warn?: (message: string) => void, shown?: string | undefined): void;
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the connection for a connector, and write its bindings.
|
|
3
|
+
*
|
|
4
|
+
* `prepare` already knows which connector it is preparing. Making the author go
|
|
5
|
+
* and find that connector's connection id by hand — and then its folder key, and
|
|
6
|
+
* then hand-write `bindings.json` around both — is work the command can do
|
|
7
|
+
* itself from information it already has. Measured on the eval corpus, that
|
|
8
|
+
* hand-work was 4 to 7 tool calls per task.
|
|
9
|
+
*
|
|
10
|
+
* THE ONE THING THIS MUST NOT DO IS GUESS. A tenant can carry several
|
|
11
|
+
* connections for the same connector — the test tenant has five for Slack — and
|
|
12
|
+
* they are not interchangeable: a user who exists in one workspace does not
|
|
13
|
+
* exist in another, so picking the wrong one makes a lookup unresolvable for a
|
|
14
|
+
* reason that points nowhere near the actual mistake. That failure was observed
|
|
15
|
+
* directly (`skill-flow-slack-dm-lookup` named the wrong workspace and burned
|
|
16
|
+
* its whole turn budget), which is why ambiguity here is an error listing the
|
|
17
|
+
* candidates and never a silent choice.
|
|
18
|
+
*
|
|
19
|
+
* Ported from `integrations/scripts/_connections.py`. The decisions are pure
|
|
20
|
+
* functions over an already-fetched listing; the `async` wrappers fetch through
|
|
21
|
+
* a {@link UipClient} and hand the rows to them. Every refusal's text is kept
|
|
22
|
+
* verbatim — agents read these messages and follow them.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { pyJsonStringify } from "../codecs/pyjson.js";
|
|
26
|
+
import { pyStrip } from "../compat/pystr.js";
|
|
27
|
+
import { Refusal, messageOf } from "../errors.js";
|
|
28
|
+
import { isConnectionsList } from "../uip/commands.js";
|
|
29
|
+
import { dataItems, parseEnvelope } from "../uip/envelope.js";
|
|
30
|
+
import { isPlainObject, notJsonReason, pyGet, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
|
|
31
|
+
/** No usable connection, with the reason and the way forward (`_connections.ConnectionError_`). */
|
|
32
|
+
export class ConnectionRefusal extends Refusal {
|
|
33
|
+
constructor(message, code) {
|
|
34
|
+
super(message, code);
|
|
35
|
+
this.name = "ConnectionRefusal";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `uip is connections list --output json`, as rows.
|
|
40
|
+
*
|
|
41
|
+
* A non-zero exit and a non-JSON reply are both refusals, never an empty list:
|
|
42
|
+
* "you have no connection" and "the listing failed" have opposite remedies.
|
|
43
|
+
*/
|
|
44
|
+
export async function listConnections(client, options) {
|
|
45
|
+
const result = await client.run(isConnectionsList({ allFolders: options.allFolders, byoa: options.byoa }));
|
|
46
|
+
if (result.code !== 0) {
|
|
47
|
+
throw new ConnectionRefusal(`listing connections failed: ${pyStrip(result.stderr) || "no output"}`, "CONNECTION_LISTING_FAILED");
|
|
48
|
+
}
|
|
49
|
+
const envelope = parseEnvelope(result.stdout);
|
|
50
|
+
if (envelope === undefined) {
|
|
51
|
+
throw new ConnectionRefusal(`connection listing was not JSON: ${notJsonReason(result.stdout)}`, "CONNECTION_LISTING_NOT_JSON");
|
|
52
|
+
}
|
|
53
|
+
return dataItems(envelope).filter(isPlainObject);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The rows for `connectorKey` that can serve a request, optionally by `Name`.
|
|
57
|
+
*
|
|
58
|
+
* A Failed connection is listed like any other and cannot serve a request;
|
|
59
|
+
* selecting one produces an error about the operation rather than about the
|
|
60
|
+
* connection, which sends the reader the wrong way.
|
|
61
|
+
*/
|
|
62
|
+
export function enabledConnections(rows, connectorKey, name) {
|
|
63
|
+
let out = rows.filter((row) => row.ConnectorKey === connectorKey && pyStrAny(pyGet(row, "State", "")).toLowerCase() === "enabled");
|
|
64
|
+
if (name)
|
|
65
|
+
out = out.filter((row) => row.Name === name);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The candidates, one pasteable `--connection-id` line each
|
|
70
|
+
* (`_connections.candidate_listing`): `<indent><Name> --connection-id <Id> (folder: <Folder>)`.
|
|
71
|
+
*/
|
|
72
|
+
export function candidateListing(rows, indent = " ") {
|
|
73
|
+
return rows
|
|
74
|
+
.map((row) => `${indent}${pyStrAny(row.Name)} --connection-id ${pyStrAny(row.Id)} (folder: ${pyStrAny(row.Folder)})`)
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
/** {@link candidateListing} at the refusals' four-space indent. */
|
|
78
|
+
function candidateLines(rows) {
|
|
79
|
+
return candidateListing(rows);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The one enabled connection for `connectorKey` among `rows`, or throw.
|
|
83
|
+
*
|
|
84
|
+
* Pure: `rows` is whatever listing the caller fetched, and `scope` is how the
|
|
85
|
+
* refusal names it. Several candidates is a refusal that lists every one with
|
|
86
|
+
* its id — a NAME is not a key on a tenant. The eval tenant holds three Slack
|
|
87
|
+
* connections all named `is-sandboxes` (two Enabled, in different folders), so
|
|
88
|
+
* the old remedy — "name the one you want with --connection <name>" — handed
|
|
89
|
+
* back the very flag the caller had just used, and the doc-following author
|
|
90
|
+
* dead-ended (placeholder GUIDs hand-written into bindings.json, 401 at the
|
|
91
|
+
* Slack node).
|
|
92
|
+
*/
|
|
93
|
+
export function selectConnection(rows, connectorKey, name, scope = "the tenant") {
|
|
94
|
+
const matched = enabledConnections(rows, connectorKey, name);
|
|
95
|
+
if (matched.length === 0) {
|
|
96
|
+
const qualifier = name ? ` named ${pyRepr(name)}` : "";
|
|
97
|
+
throw new ConnectionRefusal(`no enabled ${connectorKey} connection${qualifier} found in ${scope}. ` +
|
|
98
|
+
"List what exists with:\n" +
|
|
99
|
+
" uip is connections list --all-folders", "CONNECTION_NONE");
|
|
100
|
+
}
|
|
101
|
+
if (matched.length > 1) {
|
|
102
|
+
const listed = candidateLines(matched);
|
|
103
|
+
const names = new Set(matched.map((row) => pyStrAny(row.Name)));
|
|
104
|
+
// Python tested `name is not None`: an empty-string name takes this branch too.
|
|
105
|
+
const remedy = (name !== undefined && name !== null) || names.size < matched.length
|
|
106
|
+
? "They share a name, so --connection <name> cannot pick one; pass " +
|
|
107
|
+
"the id of the one in the folder you mean with --connection-id:"
|
|
108
|
+
: "Name the one you want with --connection <name>, or pass its id " +
|
|
109
|
+
"with --connection-id:";
|
|
110
|
+
throw new ConnectionRefusal(`${matched.length} enabled ${connectorKey} connections in ${scope}, and they ` +
|
|
111
|
+
"are not interchangeable — a record that exists in one workspace need " +
|
|
112
|
+
`not exist in another. ${remedy}\n${listed}`, "CONNECTION_AMBIGUOUS");
|
|
113
|
+
}
|
|
114
|
+
return matched[0];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The one enabled connection for `connectorKey`, fetched, or throw.
|
|
118
|
+
*
|
|
119
|
+
* Tries the DEFAULT listing first and only widens to `--all-folders` if that
|
|
120
|
+
* finds nothing. That order matters and is not just politeness to the API: the
|
|
121
|
+
* default listing is the caller's own folder, which on a real tenant is one
|
|
122
|
+
* connection where the tenant-wide view is many. Widening first would turn an
|
|
123
|
+
* unambiguous case into an ambiguous one.
|
|
124
|
+
*/
|
|
125
|
+
export async function discoverConnection(client, connectorKey, name) {
|
|
126
|
+
let rows = enabledConnections(await listConnections(client, { allFolders: false }), connectorKey, name);
|
|
127
|
+
let scope = "your folder";
|
|
128
|
+
if (rows.length === 0) {
|
|
129
|
+
rows = enabledConnections(await listConnections(client, { allFolders: true }), connectorKey, name);
|
|
130
|
+
scope = "the tenant";
|
|
131
|
+
}
|
|
132
|
+
return selectConnection(rows, connectorKey, name, scope);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Every enabled connection for `connectorKey`, own folder FIRST
|
|
136
|
+
* (`_connections.candidate_connections`).
|
|
137
|
+
*
|
|
138
|
+
* {@link discoverConnection} answers "which one" and stops at the first
|
|
139
|
+
* non-empty scope, which is right for picking a connection and wrong for
|
|
140
|
+
* retrying one: when the own-folder listing holds exactly one, the tenant-wide
|
|
141
|
+
* connections are never even looked at. So a lookup that exhausts the
|
|
142
|
+
* personal-workspace connection reports a bad VALUE while the record sits in
|
|
143
|
+
* another folder's connection (flow-builder-sdk#744, measured on two Jira
|
|
144
|
+
* connections where only the second carried project `TS`).
|
|
145
|
+
*
|
|
146
|
+
* Ordering is the same preference discovery encodes — the caller's own folder
|
|
147
|
+
* is the likeliest and is tried first — but nothing is dropped. Deduplicated
|
|
148
|
+
* by `Id`, because the tenant-wide listing repeats the own-folder rows; a row
|
|
149
|
+
* with no `Id` is never deduplicated, as the Python's `if identifier and …`
|
|
150
|
+
* did not.
|
|
151
|
+
*
|
|
152
|
+
* This does NOT decide anything, and in particular does not throw on several
|
|
153
|
+
* matches: choosing among them is the caller's job, and the only honest way to
|
|
154
|
+
* choose is evidence (a lookup that resolves on exactly one). That keeps this
|
|
155
|
+
* module's rule intact — ambiguity is never resolved by guessing.
|
|
156
|
+
*/
|
|
157
|
+
export async function candidateConnections(client, connectorKey, name) {
|
|
158
|
+
const ordered = [];
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
for (const allFolders of [false, true]) {
|
|
161
|
+
for (const row of enabledConnections(await listConnections(client, { allFolders }), connectorKey, name)) {
|
|
162
|
+
const identifier = pyStrAny(pyGet(row, "Id", ""));
|
|
163
|
+
if (identifier && seen.has(identifier))
|
|
164
|
+
continue;
|
|
165
|
+
seen.add(identifier);
|
|
166
|
+
ordered.push(row);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return ordered;
|
|
170
|
+
}
|
|
171
|
+
/** The enabled connections for `connectorKey`, one per line, pasteable. */
|
|
172
|
+
export function enabledListing(rows, connectorKey) {
|
|
173
|
+
const enabled = enabledConnections(rows, connectorKey);
|
|
174
|
+
if (enabled.length === 0) {
|
|
175
|
+
return (` (none — this tenant has no Enabled ${connectorKey} connection; ` +
|
|
176
|
+
"repair or create one in Integration Service first)");
|
|
177
|
+
}
|
|
178
|
+
return candidateLines(enabled);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The record for an explicitly named connection among `rows`, or throw unless it can serve.
|
|
182
|
+
*
|
|
183
|
+
* An explicit `--connection-id` used to skip every check discovery applies, and
|
|
184
|
+
* that skip is where a whole class of silent failures came from. A connection
|
|
185
|
+
* in `State: Failed` is listed like any other, and `uip is resources describe`
|
|
186
|
+
* against it answers `Result: Success` with NO request fields — the tenant has
|
|
187
|
+
* no schema to give for a connection it cannot reach. `prepare` then wrote an
|
|
188
|
+
* overlay with zero body inputs, printed "0 input field(s)" as a success, and
|
|
189
|
+
* the next `compile` rejected every real field as unknown. Measured on the eval
|
|
190
|
+
* tenant (Salesforce `Opportunity` / `Update`): the Failed connection returned
|
|
191
|
+
* 0 request fields, the Enabled one 13.
|
|
192
|
+
*
|
|
193
|
+
* So: resolve the id against the listing, and refuse — naming the state found
|
|
194
|
+
* and the enabled alternatives — rather than describe through it.
|
|
195
|
+
*/
|
|
196
|
+
export function checkConnection(rows, connectorKey, connectionId) {
|
|
197
|
+
const match = rows.find((row) => pyStrAny(pyGet(row, "Id", "")) === pyStrAny(connectionId));
|
|
198
|
+
const alternatives = enabledListing(rows, connectorKey);
|
|
199
|
+
const remedy = "Or omit --connection-id and prepare selects the connection itself.";
|
|
200
|
+
if (match === undefined) {
|
|
201
|
+
throw new ConnectionRefusal(`connection ${connectionId} is not in this tenant's listing ` +
|
|
202
|
+
`(\`uip is connections list --all-folders\`). Enabled ${connectorKey} ` +
|
|
203
|
+
`connections:\n${alternatives}\n` +
|
|
204
|
+
remedy, "CONNECTION_UNKNOWN_ID");
|
|
205
|
+
}
|
|
206
|
+
const actualKey = match.ConnectorKey;
|
|
207
|
+
if (pyTruthy(actualKey) && actualKey !== connectorKey) {
|
|
208
|
+
throw new ConnectionRefusal(`connection ${pyStrAny(match.Name)} (${connectionId}) belongs to connector ` +
|
|
209
|
+
`${pyStrAny(actualKey)}, not ${connectorKey}. Enabled ${connectorKey} ` +
|
|
210
|
+
`connections:\n${alternatives}\n` +
|
|
211
|
+
remedy, "CONNECTION_WRONG_CONNECTOR");
|
|
212
|
+
}
|
|
213
|
+
const state = pyStrAny(pyOr(pyGet(match, "State", ""), ""));
|
|
214
|
+
if (state.toLowerCase() !== "enabled") {
|
|
215
|
+
const shown = state || "in an unknown state";
|
|
216
|
+
throw new ConnectionRefusal(`connection ${pyStrAny(match.Name)} (${connectionId}) is ${shown}, not ` +
|
|
217
|
+
"Enabled. A connection that is not Enabled answers field discovery " +
|
|
218
|
+
"with an EMPTY schema, so the overlay would carry no fields and the " +
|
|
219
|
+
"next compile would still reject every input as unknown. Enabled " +
|
|
220
|
+
`${connectorKey} connections:\n${alternatives}\n` +
|
|
221
|
+
remedy, "CONNECTION_NOT_ENABLED");
|
|
222
|
+
}
|
|
223
|
+
return match;
|
|
224
|
+
}
|
|
225
|
+
/** {@link checkConnection} against the tenant-wide listing — the id may live in another folder. */
|
|
226
|
+
export async function lookupConnection(client, connectorKey, connectionId) {
|
|
227
|
+
return checkConnection(await listConnections(client, { allFolders: true }), connectorKey, connectionId);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The enabled BYOA connections for `connectorKey`.
|
|
231
|
+
*
|
|
232
|
+
* THROWS on a failed listing rather than answering an empty list. The
|
|
233
|
+
* difference is the whole point: "you have no BYOA connection" tells the caller
|
|
234
|
+
* to go create one, and a listing that failed says nothing of the kind.
|
|
235
|
+
* Answering [] for both turns a broken call into confident, wrong advice.
|
|
236
|
+
*/
|
|
237
|
+
export async function byoaConnections(client, connectorKey) {
|
|
238
|
+
return enabledConnections(await listConnections(client, { allFolders: true, byoa: true }), connectorKey);
|
|
239
|
+
}
|
|
240
|
+
/** The BYOA candidates, one pasteable line each, or why there are none. */
|
|
241
|
+
export function byoaListing(rows, connectorKey) {
|
|
242
|
+
if (rows.length === 0) {
|
|
243
|
+
return (` (none — this tenant has no enabled BYOA ${connectorKey} ` +
|
|
244
|
+
"connection; create one in Integration Service with your own " +
|
|
245
|
+
"OAuth app, then re-run)");
|
|
246
|
+
}
|
|
247
|
+
return candidateLines(rows);
|
|
248
|
+
}
|
|
249
|
+
function requiredKey(connection, key) {
|
|
250
|
+
if (!Object.prototype.hasOwnProperty.call(connection, key)) {
|
|
251
|
+
throw new Error(`connection record has no ${pyRepr(key)}`); // Python: KeyError
|
|
252
|
+
}
|
|
253
|
+
return pyStrAny(connection[key]);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Add connection + folder entries to a parsed `bindings.json`, without touching the input.
|
|
257
|
+
*
|
|
258
|
+
* ADDITIVE AND IDEMPOTENT. An existing entry under the same symbolic name (its
|
|
259
|
+
* `name` or `id`) is updated, never duplicated, and every other entry is left
|
|
260
|
+
* exactly as it was — `bindings.json` is an authored file that may already
|
|
261
|
+
* carry bindings for connectors this run knows nothing about. An updated row
|
|
262
|
+
* keeps its key order and gains any missing key at the end; a new row is
|
|
263
|
+
* written `id, name, resource, resourceKey, default, propertyAttribute`.
|
|
264
|
+
*
|
|
265
|
+
* `document` is what the file parsed to, or `undefined` when there was no file.
|
|
266
|
+
* As in the Python, a parsed value that is not an object is replaced by the
|
|
267
|
+
* default document.
|
|
268
|
+
*/
|
|
269
|
+
export function mergeBindingsDocument(document, symbolicConnection, symbolicFolder, connection) {
|
|
270
|
+
let out;
|
|
271
|
+
if (isPlainObject(document)) {
|
|
272
|
+
const copy = { ...document };
|
|
273
|
+
if (!Object.prototype.hasOwnProperty.call(copy, "schemaVersion"))
|
|
274
|
+
copy.schemaVersion = "1";
|
|
275
|
+
if (!Object.prototype.hasOwnProperty.call(copy, "bindings"))
|
|
276
|
+
copy.bindings = [];
|
|
277
|
+
if (!Array.isArray(copy.bindings)) {
|
|
278
|
+
throw new TypeError(`bindings.json: "bindings" is not a list`); // Python: TypeError iterating it
|
|
279
|
+
}
|
|
280
|
+
copy.bindings = copy.bindings.map((entry) => (isPlainObject(entry) ? { ...entry } : entry));
|
|
281
|
+
out = copy;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
out = { schemaVersion: "1", bindings: [] };
|
|
285
|
+
}
|
|
286
|
+
const bindings = out.bindings;
|
|
287
|
+
const upsert = (symbolic, key, attribute) => {
|
|
288
|
+
for (const entry of bindings) {
|
|
289
|
+
if (!isPlainObject(entry))
|
|
290
|
+
continue;
|
|
291
|
+
if (entry.name === symbolic || entry.id === symbolic) {
|
|
292
|
+
const changed = entry.resourceKey !== key;
|
|
293
|
+
Object.assign(entry, {
|
|
294
|
+
id: pyGet(entry, "id", symbolic),
|
|
295
|
+
name: symbolic,
|
|
296
|
+
resource: "Connection",
|
|
297
|
+
resourceKey: key,
|
|
298
|
+
default: key,
|
|
299
|
+
propertyAttribute: attribute,
|
|
300
|
+
});
|
|
301
|
+
return changed;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
bindings.push({
|
|
305
|
+
id: symbolic,
|
|
306
|
+
name: symbolic,
|
|
307
|
+
resource: "Connection",
|
|
308
|
+
resourceKey: key,
|
|
309
|
+
default: key,
|
|
310
|
+
propertyAttribute: attribute,
|
|
311
|
+
});
|
|
312
|
+
return true;
|
|
313
|
+
};
|
|
314
|
+
const wroteConnection = upsert(symbolicConnection, requiredKey(connection, "Id"), "ConnectionId");
|
|
315
|
+
const wroteFolder = upsert(symbolicFolder, requiredKey(connection, "FolderKey"), "FolderKey");
|
|
316
|
+
return { document: out, wroteConnection, wroteFolder };
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* `json.dump(document, indent=2)` + newline: the bytes `bindings.json` is written in.
|
|
320
|
+
*
|
|
321
|
+
* `allowIntegerKeys` for the same reason the ledger passes it (`objects.ts`):
|
|
322
|
+
* `bindings.json` is the author's file and may carry an integer-like key
|
|
323
|
+
* anywhere in a row this module never looks at. `json.dump` wrote such a file
|
|
324
|
+
* back; refusing it here left the file untouched and the connection unbound,
|
|
325
|
+
* with an error about a key the author did not know they had. The one thing
|
|
326
|
+
* that cannot be kept is that key's position — `JSON.parse` has already
|
|
327
|
+
* hoisted it to the front of its object — which Python preserved.
|
|
328
|
+
*/
|
|
329
|
+
export function formatBindings(document) {
|
|
330
|
+
return `${pyJsonStringify(document, { indent: 2, allowIntegerKeys: true })}\n`;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Add connection + folder entries to `bindings.json` at `path`, in place.
|
|
334
|
+
*
|
|
335
|
+
* Returns `[wroteConnection, wroteFolder]` describing what changed, so the
|
|
336
|
+
* caller can report the specific thing rather than "updated bindings". The file
|
|
337
|
+
* is always rewritten, as the Python did. A file that will not parse is REFUSED,
|
|
338
|
+
* not overwritten: it is the author's, and not ours to discard.
|
|
339
|
+
*/
|
|
340
|
+
export function mergeBindings(path, symbolicConnection, symbolicFolder, connection) {
|
|
341
|
+
let loaded = undefined;
|
|
342
|
+
if (existsSync(path)) {
|
|
343
|
+
try {
|
|
344
|
+
loaded = JSON.parse(readFileSync(path, "utf8"));
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
throw new ConnectionRefusal(`${path} exists but is not readable JSON. Fix or remove it; ` +
|
|
348
|
+
"this command will not overwrite a file it cannot parse.", "BINDINGS_UNREADABLE");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const merged = mergeBindingsDocument(loaded, symbolicConnection, symbolicFolder, connection);
|
|
352
|
+
writeFileSync(path, formatBindings(merged.document), "utf8");
|
|
353
|
+
return [merged.wroteConnection, merged.wroteFolder];
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Remember `bindings.json` byte-for-byte, before a stage that writes it.
|
|
357
|
+
*
|
|
358
|
+
* `undefined` means the file could not be read — normally that it did not
|
|
359
|
+
* exist — and {@link restoreBindings} will remove whatever was written since.
|
|
360
|
+
*/
|
|
361
|
+
export function snapshotBindings(path) {
|
|
362
|
+
if (!path)
|
|
363
|
+
return undefined;
|
|
364
|
+
try {
|
|
365
|
+
return new Uint8Array(readFileSync(path));
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Put `bindings.json` back exactly as {@link snapshotBindings} found it.
|
|
373
|
+
*
|
|
374
|
+
* A refusal must leave `bindings.json` as it was: the BYOA fact needs a
|
|
375
|
+
* connection to discover, so stage 0 has already written one by the time the
|
|
376
|
+
* event says it is the wrong KIND — without the rollback the overlay keeps
|
|
377
|
+
* pointing at a connection the tool just forbade. A failure to restore is
|
|
378
|
+
* reported through `warn` (stderr by default), never thrown: the refusal being
|
|
379
|
+
* delivered is the message that matters.
|
|
380
|
+
*
|
|
381
|
+
* `shown` is the path as the caller typed it (`args.bindings`), which is what
|
|
382
|
+
* the Python printed; `path` is where the file actually is.
|
|
383
|
+
*/
|
|
384
|
+
export function restoreBindings(path, snapshot, warn = (message) => process.stderr.write(`${message}\n`), shown = path) {
|
|
385
|
+
if (!path)
|
|
386
|
+
return;
|
|
387
|
+
try {
|
|
388
|
+
if (snapshot === undefined) {
|
|
389
|
+
if (existsSync(path))
|
|
390
|
+
rmSync(path);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
writeFileSync(path, snapshot);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
warn(` WARN: could not restore ${shown} after refusing the connection: ${messageOf(error)}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type BuildContext } from "../library-build/context.js";
|
|
2
|
+
import { type EventFacts } from "../library-build/events.js";
|
|
3
|
+
import { type Dict } from "../library-build/py.js";
|
|
4
|
+
import type { LibraryEntry } from "../model/library.js";
|
|
5
|
+
import type { UipClient } from "../uip/client.js";
|
|
6
|
+
/**
|
|
7
|
+
* `_event_operation`: the operation key every trigger API takes.
|
|
8
|
+
*
|
|
9
|
+
* NOT `operation.name`: that is the object's CRUD verb (`Create` on outlook
|
|
10
|
+
* `email-received`), while the event's own operation is `EMAIL_RECEIVED`,
|
|
11
|
+
* which `extractCanonical` stamps as `eventOperation` from the manifest's
|
|
12
|
+
* `model.context`. `name` happens to fall back to the same value when the
|
|
13
|
+
* manifest omits `connectorMethodInfo.operation`, so reading it works or
|
|
14
|
+
* silently queries the WRONG operation depending on which fallback rung a
|
|
15
|
+
* connector's manifest exercises.
|
|
16
|
+
*/
|
|
17
|
+
export declare function eventOperation(operation: Record<string, unknown>): string;
|
|
18
|
+
/** The START and WAIT node types of one event. */
|
|
19
|
+
export declare function eventNodeTypes(connectorKey: string, action: string): {
|
|
20
|
+
trigger: string;
|
|
21
|
+
event: string;
|
|
22
|
+
};
|
|
23
|
+
/** The stored definition and its library entry, both re-keyed to the WAIT form. */
|
|
24
|
+
export interface EventDefinition {
|
|
25
|
+
node: Dict;
|
|
26
|
+
entry: LibraryEntry;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The one stored shape (see the module comment): the definition with the
|
|
30
|
+
* describe folded in, or with the object row's `Parameters` unioned into its
|
|
31
|
+
* own `eventParameters`; the entry's `outputSchema` taken from the definition's
|
|
32
|
+
* `outputResponseDefinition` when that has fields. Pure — copies of both inputs.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assembleEventDefinition(node: Dict, entry: LibraryEntry, describe: Dict | undefined, objectRow: Record<string, unknown> | undefined, eventNodeType: string): EventDefinition;
|
|
35
|
+
/** What stage 0 wrote to `bindings.json`, so a refusal here can undo exactly that. */
|
|
36
|
+
export interface BindingsRollback {
|
|
37
|
+
path: string;
|
|
38
|
+
/** The file before stage 0 touched it; `undefined` when it did not exist. */
|
|
39
|
+
snapshot: Uint8Array | undefined;
|
|
40
|
+
/** `path` as the caller typed `--bindings`, for the lines that name it; defaults to `path`. */
|
|
41
|
+
shown?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `_refuse_unless_byoa_connection`: refuse a standard connection on an event
|
|
45
|
+
* object that requires BYOA. V1 measured the failure this prevents — the
|
|
46
|
+
* trigger deploys and then faults at runtime with a webhook error naming
|
|
47
|
+
* nothing about connections — and the requirement is knowable now.
|
|
48
|
+
*
|
|
49
|
+
* A refusal must leave nothing behind: stage 0 has already written this
|
|
50
|
+
* connection into `bindings.json` by the time the event's own metadata says it
|
|
51
|
+
* is the wrong KIND, so the file is restored first. A listing that FAILED is
|
|
52
|
+
* not a tenant with no BYOA connections; the remedies are opposite.
|
|
53
|
+
*/
|
|
54
|
+
export declare function refuseUnlessByoaConnection(client: UipClient, connectorKey: string, connectionId: string, rollback: BindingsRollback | undefined, warn: (line: string) => void): Promise<void>;
|
|
55
|
+
export interface EventPrepareOptions {
|
|
56
|
+
connectorKey: string;
|
|
57
|
+
action: string;
|
|
58
|
+
connectionId: string;
|
|
59
|
+
/** `--object`: the ONE object a Generic event fires for. */
|
|
60
|
+
objectName?: string | null;
|
|
61
|
+
allObjects?: boolean;
|
|
62
|
+
/** The raw `-f` flags — an event has no parent fields, so any is a refusal. */
|
|
63
|
+
fields?: readonly string[];
|
|
64
|
+
resolve?: readonly string[];
|
|
65
|
+
refreshLookups?: boolean;
|
|
66
|
+
/** The overlay directory to write (resolved). */
|
|
67
|
+
out: string;
|
|
68
|
+
/** `out` as the caller spelled `--out`; every printed line names this, as the Python printed `args.out`. Defaults to `out`. */
|
|
69
|
+
outShown?: string;
|
|
70
|
+
importSpecifier: string;
|
|
71
|
+
jsonOnly?: boolean;
|
|
72
|
+
/** The symbolic names stage 0 bound, for the `next:` subscription. */
|
|
73
|
+
boundConnection?: string | null;
|
|
74
|
+
boundFolder?: string | null;
|
|
75
|
+
bindings?: BindingsRollback;
|
|
76
|
+
/** How the pasteable `registry prepare` is spelled in the vocabulary print. */
|
|
77
|
+
prepareCommand?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface EventPrepareResult {
|
|
80
|
+
eventNodeType: string;
|
|
81
|
+
operation: string;
|
|
82
|
+
generic: boolean;
|
|
83
|
+
/** The API's spelling of the object, for a generic event. */
|
|
84
|
+
objectName?: string;
|
|
85
|
+
facts: EventFacts;
|
|
86
|
+
whereFields: unknown[];
|
|
87
|
+
filterFields: unknown[];
|
|
88
|
+
definition: EventDefinition;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `_prepare_event`. Returns `undefined` when `<key> <action>` is not an event
|
|
92
|
+
* either, so the caller can raise the ordinary unknown-action refusal;
|
|
93
|
+
* everything else is handled here, refusals included. `ctx.connectionId` must
|
|
94
|
+
* already be the connection: the trigger definition only answers with one.
|
|
95
|
+
*/
|
|
96
|
+
export declare function prepareEvent(ctx: BuildContext, options: EventPrepareOptions): Promise<EventPrepareResult | undefined>;
|