@uipath/maestro-builder-sdk 6.4.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/cli/commands/registry.js +68 -12
  2. package/dist/registry/archive/entry-policy.d.ts +36 -0
  3. package/dist/registry/archive/entry-policy.js +60 -0
  4. package/dist/registry/archive/extract.d.ts +31 -0
  5. package/dist/registry/archive/extract.js +92 -0
  6. package/dist/registry/archive/zip-reader.d.ts +38 -0
  7. package/dist/registry/archive/zip-reader.js +256 -0
  8. package/dist/registry/codecs/pyjson.d.ts +62 -0
  9. package/dist/registry/codecs/pyjson.js +208 -0
  10. package/dist/registry/compat/compare.d.ts +18 -0
  11. package/dist/registry/compat/compare.js +32 -0
  12. package/dist/registry/compat/paths.d.ts +4 -0
  13. package/dist/registry/compat/paths.js +17 -0
  14. package/dist/registry/compat/pystr.d.ts +50 -0
  15. package/dist/registry/compat/pystr.js +221 -0
  16. package/dist/registry/concurrency.d.ts +10 -0
  17. package/dist/registry/concurrency.js +34 -0
  18. package/dist/registry/curated-overlay.d.ts +36 -4
  19. package/dist/registry/curated-overlay.js +87 -14
  20. package/dist/registry/errors.d.ts +26 -0
  21. package/dist/registry/errors.js +38 -0
  22. package/dist/registry/extractor.d.ts +9 -10
  23. package/dist/registry/extractor.js +5 -15
  24. package/dist/registry/files.d.ts +26 -0
  25. package/dist/registry/files.js +160 -0
  26. package/dist/registry/generators/descriptors.d.ts +81 -0
  27. package/dist/registry/generators/descriptors.js +405 -0
  28. package/dist/registry/generators/fields.d.ts +46 -0
  29. package/dist/registry/generators/fields.js +111 -0
  30. package/dist/registry/generators/markdown/common.d.ts +21 -0
  31. package/dist/registry/generators/markdown/common.js +55 -0
  32. package/dist/registry/generators/markdown/index.d.ts +33 -0
  33. package/dist/registry/generators/markdown/index.js +37 -0
  34. package/dist/registry/generators/markdown/page.d.ts +8 -0
  35. package/dist/registry/generators/markdown/page.js +52 -0
  36. package/dist/registry/generators/markdown/pyvalue.d.ts +34 -0
  37. package/dist/registry/generators/markdown/pyvalue.js +106 -0
  38. package/dist/registry/generators/markdown/sections/choosing-object.d.ts +2 -0
  39. package/dist/registry/generators/markdown/sections/choosing-object.js +52 -0
  40. package/dist/registry/generators/markdown/sections/example.d.ts +22 -0
  41. package/dist/registry/generators/markdown/sections/example.js +295 -0
  42. package/dist/registry/generators/markdown/sections/filtering.d.ts +2 -0
  43. package/dist/registry/generators/markdown/sections/filtering.js +76 -0
  44. package/dist/registry/generators/markdown/sections/header.d.ts +2 -0
  45. package/dist/registry/generators/markdown/sections/header.js +37 -0
  46. package/dist/registry/generators/markdown/sections/inputs.d.ts +30 -0
  47. package/dist/registry/generators/markdown/sections/inputs.js +151 -0
  48. package/dist/registry/generators/markdown/sections/lookups.d.ts +12 -0
  49. package/dist/registry/generators/markdown/sections/lookups.js +150 -0
  50. package/dist/registry/generators/markdown/sections/outputs.d.ts +3 -0
  51. package/dist/registry/generators/markdown/sections/outputs.js +9 -0
  52. package/dist/registry/generators/markdown/sections/resolving-fields.d.ts +2 -0
  53. package/dist/registry/generators/markdown/sections/resolving-fields.js +139 -0
  54. package/dist/registry/generators/markdown/sections/schema-dynamic.d.ts +2 -0
  55. package/dist/registry/generators/markdown/sections/schema-dynamic.js +86 -0
  56. package/dist/registry/generators/materialize.d.ts +30 -0
  57. package/dist/registry/generators/materialize.js +132 -0
  58. package/dist/registry/generators/md-symbols.d.ts +22 -0
  59. package/dist/registry/generators/md-symbols.js +58 -0
  60. package/dist/registry/generators/triggers.d.ts +15 -0
  61. package/dist/registry/generators/triggers.js +85 -0
  62. package/dist/registry/generators/type-registry.d.ts +50 -0
  63. package/dist/registry/generators/type-registry.js +154 -0
  64. package/dist/registry/generators/type-tree.d.ts +65 -0
  65. package/dist/registry/generators/type-tree.js +179 -0
  66. package/dist/registry/library-build/build.d.ts +57 -0
  67. package/dist/registry/library-build/build.js +286 -0
  68. package/dist/registry/library-build/canonical.d.ts +119 -0
  69. package/dist/registry/library-build/canonical.js +512 -0
  70. package/dist/registry/library-build/cli.d.ts +45 -0
  71. package/dist/registry/library-build/cli.js +182 -0
  72. package/dist/registry/library-build/context.d.ts +107 -0
  73. package/dist/registry/library-build/context.js +267 -0
  74. package/dist/registry/library-build/enrich.d.ts +36 -0
  75. package/dist/registry/library-build/enrich.js +158 -0
  76. package/dist/registry/library-build/events.d.ts +108 -0
  77. package/dist/registry/library-build/events.js +476 -0
  78. package/dist/registry/library-build/generic-expand.d.ts +77 -0
  79. package/dist/registry/library-build/generic-expand.js +363 -0
  80. package/dist/registry/library-build/plan.d.ts +71 -0
  81. package/dist/registry/library-build/plan.js +270 -0
  82. package/dist/registry/library-build/py.d.ts +45 -0
  83. package/dist/registry/library-build/py.js +167 -0
  84. package/dist/registry/library-build/schema-action.d.ts +38 -0
  85. package/dist/registry/library-build/schema-action.js +107 -0
  86. package/dist/registry/library-build/write.d.ts +6 -0
  87. package/dist/registry/library-build/write.js +26 -0
  88. package/dist/registry/library-reader.d.ts +20 -0
  89. package/dist/registry/library-reader.js +50 -0
  90. package/dist/registry/lookup-specs.d.ts +55 -0
  91. package/dist/registry/lookup-specs.js +140 -0
  92. package/dist/registry/materialize-cli.js +28 -14
  93. package/dist/registry/materializer.d.ts +25 -3
  94. package/dist/registry/materializer.js +45 -29
  95. package/dist/registry/model/library.d.ts +254 -0
  96. package/dist/registry/model/library.js +23 -0
  97. package/dist/registry/naming.d.ts +54 -0
  98. package/dist/registry/naming.js +142 -0
  99. package/dist/registry/orphans.d.ts +4 -0
  100. package/dist/registry/orphans.js +86 -0
  101. package/dist/registry/prepare/actions.d.ts +26 -0
  102. package/dist/registry/prepare/actions.js +94 -0
  103. package/dist/registry/prepare/connections.d.ts +181 -0
  104. package/dist/registry/prepare/connections.js +399 -0
  105. package/dist/registry/prepare/event.d.ts +96 -0
  106. package/dist/registry/prepare/event.js +268 -0
  107. package/dist/registry/prepare/handoff.d.ts +21 -0
  108. package/dist/registry/prepare/handoff.js +91 -0
  109. package/dist/registry/prepare/ledger.d.ts +104 -0
  110. package/dist/registry/prepare/ledger.js +216 -0
  111. package/dist/registry/prepare/messages.d.ts +190 -0
  112. package/dist/registry/prepare/messages.js +472 -0
  113. package/dist/registry/prepare/objects.d.ts +130 -0
  114. package/dist/registry/prepare/objects.js +318 -0
  115. package/dist/registry/prepare/overlay.d.ts +149 -0
  116. package/dist/registry/prepare/overlay.js +487 -0
  117. package/dist/registry/prepare/parents.d.ts +41 -0
  118. package/dist/registry/prepare/parents.js +122 -0
  119. package/dist/registry/prepare/pipeline.d.ts +91 -0
  120. package/dist/registry/prepare/pipeline.js +376 -0
  121. package/dist/registry/prepare/regenerate.d.ts +9 -0
  122. package/dist/registry/prepare/regenerate.js +50 -0
  123. package/dist/registry/prepare/resolve.d.ts +199 -0
  124. package/dist/registry/prepare/resolve.js +470 -0
  125. package/dist/registry/prepare/retry.d.ts +83 -0
  126. package/dist/registry/prepare/retry.js +109 -0
  127. package/dist/registry/prepare/source-import.d.ts +54 -0
  128. package/dist/registry/prepare/source-import.js +192 -0
  129. package/dist/registry/prepare/support.d.ts +41 -0
  130. package/dist/registry/prepare/support.js +178 -0
  131. package/dist/registry/prepare-cli.d.ts +16 -5
  132. package/dist/registry/prepare-cli.js +60 -53
  133. package/dist/registry/pull-cli.js +8 -7
  134. package/dist/registry/reporter.d.ts +26 -0
  135. package/dist/registry/reporter.js +41 -0
  136. package/dist/registry/uip/client.d.ts +99 -0
  137. package/dist/registry/uip/client.js +226 -0
  138. package/dist/registry/uip/commands.d.ts +40 -0
  139. package/dist/registry/uip/commands.js +80 -0
  140. package/dist/registry/uip/envelope.d.ts +37 -0
  141. package/dist/registry/uip/envelope.js +84 -0
  142. package/dist/registry/uip/response-cache.d.ts +28 -0
  143. package/dist/registry/uip/response-cache.js +57 -0
  144. package/lib/README.md +10 -8
  145. package/package.json +3 -3
  146. package/dist/generators/_connections.py +0 -331
  147. package/dist/generators/_events.py +0 -355
  148. package/dist/generators/_lookups.py +0 -114
  149. package/dist/generators/_naming.py +0 -185
  150. package/dist/generators/_objects.py +0 -299
  151. package/dist/generators/_resolve.py +0 -319
  152. package/dist/generators/convert_library_to_md.py +0 -1441
  153. package/dist/generators/extract_library.py +0 -125
  154. package/dist/generators/generate_connectors_ts.py +0 -936
  155. package/dist/generators/generate_library_json.py +0 -2527
  156. package/dist/generators/generate_triggers_ts.py +0 -110
  157. package/dist/generators/materialize_registry.py +0 -155
  158. package/dist/generators/prepare_connector.py +0 -1860
  159. package/dist/registry/python.d.ts +0 -26
  160. package/dist/registry/python.js +0 -46
@@ -0,0 +1,199 @@
1
+ import { Refusal } from "../errors.js";
2
+ import type { UipClient } from "../uip/client.js";
3
+ /**
4
+ * A scan has to stop somewhere, and the caps must be set against the MEASURED
5
+ * page size rather than a guess. Integration Service serves **1,000 records
6
+ * per page** for a curated collection (verified against a live Slack
7
+ * connection: `curated_channels` returns 1000 with `HasMore: "true"`).
8
+ *
9
+ * The first version of this capped at 400 records — a number chosen for how
10
+ * many rows a person might scroll, which is the wrong unit entirely. It meant
11
+ * the loop broke partway through page ONE and never paginated at all. The eval
12
+ * task built to exercise this puts its target channel on page 3 (record
13
+ * ~2,001), so the resolver reported "not found" for a channel that is plainly
14
+ * there.
15
+ *
16
+ * 25 pages is ~30s of requests in the worst case, spent once, in an explicit
17
+ * command whose whole job is this. A collection with a `filterPattern` never
18
+ * reaches the scan at all.
19
+ */
20
+ export declare const PAGE_SIZE_HINT = 1000;
21
+ export declare const MAX_SCANNED: number;
22
+ export declare const PAGE_GUARD = 50;
23
+ /** A lookup that could not be resolved, with the reason a human needs (`_resolve.ResolutionError`). */
24
+ export declare class ResolutionRefusal extends Refusal {
25
+ constructor(message: string, code: string);
26
+ }
27
+ /** What {@link LookupExhausted} knows about the scan that found nothing. */
28
+ export interface LookupExhaustedFacts {
29
+ field: string;
30
+ collection: string;
31
+ by: string;
32
+ value: string;
33
+ /** Records seen across every page fetched. */
34
+ scanned: number;
35
+ /** Pages FETCHED (not transitions): the count the message prints. */
36
+ pages: number;
37
+ /** True when a cap, not the collection's end, stopped the scan. */
38
+ capped: boolean;
39
+ }
40
+ /**
41
+ * The collection was read to the end on THIS connection and had no match
42
+ * (`_resolve.LookupExhausted`).
43
+ *
44
+ * Split out from its parent because the two failures want opposite responses
45
+ * and the caller cannot tell them apart from a message. A transport or
46
+ * permission failure says nothing about whether the value exists — retrying it
47
+ * on another connection would hide a broken call behind a confident "no
48
+ * connection has it". An exhaustive miss is positive evidence about this
49
+ * connection specifically, and is the signal `prepare` uses to demote it and
50
+ * try the next candidate (flow-builder-sdk#744).
51
+ *
52
+ * `capped` flips that: a scan stopped at the record cap did NOT reach the end,
53
+ * so it is not evidence of absence and must not demote anything.
54
+ *
55
+ * The attributes carry the counts so a caller can report per-candidate results
56
+ * without re-parsing the sentence.
57
+ */
58
+ export declare class LookupExhausted extends ResolutionRefusal {
59
+ readonly field: string;
60
+ readonly collection: string;
61
+ readonly by: string;
62
+ readonly value: string;
63
+ readonly scanned: number;
64
+ readonly pages: number;
65
+ readonly capped: boolean;
66
+ constructor(message: string, facts: LookupExhaustedFacts);
67
+ }
68
+ /** One `--resolve <field>:<by>=<value>`, parsed. */
69
+ export type ResolveRequest = readonly [field: string, by: string, value: string];
70
+ /**
71
+ * What a lookup spec must say for `resolveOne`: the same fields a library
72
+ * `reference` and a `LookupSpecData` carry, read loosely.
73
+ */
74
+ export interface ResolveSpec {
75
+ path?: string;
76
+ by?: readonly string[];
77
+ value?: string;
78
+ filterPattern?: string;
79
+ dependsOn?: readonly string[];
80
+ [key: string]: unknown;
81
+ }
82
+ /**
83
+ * `channel:profile.email=dustin@example.com` -> `[field, by, value]`.
84
+ *
85
+ * Split on the FIRST colon and the FIRST `=`. Both halves can legitimately
86
+ * contain the other delimiter — `by` is a dotted path and a value can be
87
+ * anything a person typed — so a greedy split silently mis-parses real input.
88
+ */
89
+ export declare function parseResolveFlag(raw: string): ResolveRequest;
90
+ type SpecTable = ReadonlyMap<string, ResolveSpec> | Readonly<Record<string, ResolveSpec>>;
91
+ /**
92
+ * Order the requests so a `dependsOn` prerequisite resolves first.
93
+ *
94
+ * 27 fields in the shipped library are not addressable until another field is
95
+ * known — Jira's issue type needs the project, Amazon Connect's phone number
96
+ * needs the instance. Resolving in the order the flags were typed leaves an
97
+ * unsubstituted `{placeholder}` in a URL, and the error the connector returns
98
+ * for that names neither field.
99
+ *
100
+ * A field requested twice keeps its first position and its LAST `by`/`value`,
101
+ * as the Python's dict did.
102
+ */
103
+ export declare function orderResolutions(requested: readonly ResolveRequest[], specs: SpecTable): ResolveRequest[];
104
+ /**
105
+ * The records out of a `uip is resources run list` reply.
106
+ *
107
+ * The reply is `{"Result": "Success", "Code": ..., "Data": {"items": [...],
108
+ * "Pagination": {...}}}` — records live under `Data`, and `Result` is the
109
+ * STATUS STRING, not a container. Reading a top-level `items` finds nothing
110
+ * and reports "scanned 0 records" against a collection that is full, which is
111
+ * exactly what this did until an eval run walked it into the real CLI.
112
+ */
113
+ export declare function envelopeItems(payload: unknown): unknown[];
114
+ /** The `Pagination` block, from under `Data`. */
115
+ export declare function envelopePagination(payload: unknown): Record<string, unknown>;
116
+ /**
117
+ * `HasMore` arrives as the STRING "false" as often as the boolean.
118
+ *
119
+ * `if payload["Pagination"]["HasMore"]` on the string "false" is True, which
120
+ * pages forever on a collection that just said it has no more.
121
+ */
122
+ export declare function truthy(value: unknown): boolean;
123
+ /**
124
+ * The reviver that makes `JSON.parse` read a number the way `json.loads` did.
125
+ *
126
+ * Python kept every number as its text spelled it — `1.0` stayed a float,
127
+ * whose `str()` is `"1.0"`, and an integer stayed exact however many digits it
128
+ * had — so `str(got)` lost nothing on the way into `resolutions.json`.
129
+ * `JSON.parse` folds both cases: `1.0` parses to the integer `1`, and an
130
+ * integer above 2^53 is rounded to the nearest double, and the rounded number
131
+ * is what would have been recorded as the id. The reviver's third argument
132
+ * (`JSON.parse` source text access, present in every Node the package
133
+ * supports — `engines` says >= 22.6) is the spelling itself, so the two lossy
134
+ * cases are rewritten here as the string Python's `str()` would have produced.
135
+ * {@link matchRecord} and the recorded id only ever see that string, so
136
+ * nothing downstream can tell the difference — except that it is now right,
137
+ * and a typed `--resolve x:count=1.0` matches a JSON `1.0` as it did in Python.
138
+ */
139
+ export declare function reviveLikeJsonLoads(_key: string, value: unknown, context?: {
140
+ source?: string;
141
+ }): unknown;
142
+ /**
143
+ * The first JSON document in a listing's stdout, with its record values parsed
144
+ * as `json.loads` would have read them, or `undefined`.
145
+ */
146
+ export declare function parseListingDocument(text: string): unknown;
147
+ /**
148
+ * One `uip is resources run list` page, as the parsed envelope.
149
+ *
150
+ * `uip` prints a self-update failure to stderr before its real output, so
151
+ * stderr being non-empty is not evidence of anything. Judge the exit code and
152
+ * the payload.
153
+ */
154
+ export declare function listPage(client: UipClient, connectorKey: string, collection: string, connectionId: string, query: string | undefined): Promise<Record<string, unknown>>;
155
+ /** `profile.email` out of a record, or `null` (Python's `None`) when any step is missing. */
156
+ export declare function readPath(record: unknown, dotted: string): unknown;
157
+ type Resolved = ReadonlyMap<string, string> | Readonly<Record<string, string>>;
158
+ /** Fill `{fields.project.key}` placeholders from already-resolved fields, in their order. */
159
+ export declare function substitute(template: string, resolved: Resolved): string;
160
+ /**
161
+ * True when the record's `by` path, coerced with `str()`, equals `value`.
162
+ *
163
+ * The coercion is the Python's: a boolean reads `True`, a missing path `None`,
164
+ * a nested object its `repr`. It is what lets `--resolve x:active=True` match a
165
+ * JSON `true`, and it is kept exactly.
166
+ */
167
+ export declare function matchRecord(record: unknown, by: string, value: string): boolean;
168
+ /** Where a scan stopped, for the refusal that reports it. */
169
+ export interface ScanOutcome {
170
+ /** Records seen across every page fetched. */
171
+ scanned: number;
172
+ /** Page TRANSITIONS made: the number of pages fetched minus one. */
173
+ pages: number;
174
+ /** True when a cap, not the collection's end, stopped the scan. */
175
+ capped: boolean;
176
+ }
177
+ /**
178
+ * Page through a collection, yielding each page's records and returning where
179
+ * the scan stopped.
180
+ *
181
+ * Pages with exactly `nextPage=<token>` appended to the query. Guessing `page`
182
+ * / `pageToken` / `nextPageToken` is a documented way to loop forever on page
183
+ * one. Stops when the reply says there is no more, when it carries no token,
184
+ * at {@link MAX_SCANNED} records, or after {@link PAGE_GUARD} transitions.
185
+ */
186
+ export declare function scanCollection(client: UipClient, connectorKey: string, collection: string, connectionId: string, query: string | undefined): AsyncGenerator<unknown[], ScanOutcome, void>;
187
+ /**
188
+ * The connector-facing id for one lookup, or throw {@link ResolutionRefusal}.
189
+ *
190
+ * `resolved` carries the values of fields resolved earlier in this run, which
191
+ * is what makes a `dependsOn` path addressable. Parameters follow the Python
192
+ * `resolve_one(connector_key, spec, field, by, value, connection_id, resolved)`
193
+ * with the client in front.
194
+ *
195
+ * A collection read to its end with no match throws {@link LookupExhausted},
196
+ * the subclass the caller's retry keys on; every other failure is the parent.
197
+ */
198
+ export declare function resolveOne(client: UipClient, connectorKey: string, spec: ResolveSpec, field: string, by: string, value: string, connectionId: string, resolved?: Resolved): Promise<string>;
199
+ export {};
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Resolve a connector lookup field against a live connection.
3
+ *
4
+ * `prepare --resolve channel:profile.email=dustin@example.com` turns a
5
+ * human-meaningful value into the opaque id the connector wants, and records
6
+ * the answer so `compile` can read it offline. This module is the retrieval
7
+ * half.
8
+ *
9
+ * WHY IT LIVES HERE AND NOT IN COMPILE. Resolution needs the network; compile
10
+ * must not have it. The eval harness runs `check` and `compile` with no `uip`
11
+ * login, and the byte-lock gates require that the same source produces the
12
+ * same bytes — a compile that made live calls would make those gates depend on
13
+ * tenant state and they would stop meaning anything.
14
+ *
15
+ * THE STRATEGY IS DATA, NOT CODE. The spec says how to retrieve the collection:
16
+ * a `filterPattern` means one request; its absence means paging; `dependsOn`
17
+ * means another field must resolve first.
18
+ *
19
+ * Ported from `integrations/scripts/_resolve.py`; the refusal text is verbatim.
20
+ */
21
+ import { pyFloatRepr } from "../codecs/pyjson.js";
22
+ import { pyStrip, pyStripChars, splitOnce } from "../compat/pystr.js";
23
+ import { Refusal } from "../errors.js";
24
+ import { isResourcesRunList } from "../uip/commands.js";
25
+ import { isPlainObject, notJsonReason, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
26
+ /**
27
+ * A scan has to stop somewhere, and the caps must be set against the MEASURED
28
+ * page size rather than a guess. Integration Service serves **1,000 records
29
+ * per page** for a curated collection (verified against a live Slack
30
+ * connection: `curated_channels` returns 1000 with `HasMore: "true"`).
31
+ *
32
+ * The first version of this capped at 400 records — a number chosen for how
33
+ * many rows a person might scroll, which is the wrong unit entirely. It meant
34
+ * the loop broke partway through page ONE and never paginated at all. The eval
35
+ * task built to exercise this puts its target channel on page 3 (record
36
+ * ~2,001), so the resolver reported "not found" for a channel that is plainly
37
+ * there.
38
+ *
39
+ * 25 pages is ~30s of requests in the worst case, spent once, in an explicit
40
+ * command whose whole job is this. A collection with a `filterPattern` never
41
+ * reaches the scan at all.
42
+ */
43
+ export const PAGE_SIZE_HINT = 1000;
44
+ export const MAX_SCANNED = 25 * PAGE_SIZE_HINT;
45
+ export const PAGE_GUARD = 50;
46
+ /** A lookup that could not be resolved, with the reason a human needs (`_resolve.ResolutionError`). */
47
+ export class ResolutionRefusal extends Refusal {
48
+ constructor(message, code) {
49
+ super(message, code);
50
+ this.name = "ResolutionRefusal";
51
+ }
52
+ }
53
+ /**
54
+ * The collection was read to the end on THIS connection and had no match
55
+ * (`_resolve.LookupExhausted`).
56
+ *
57
+ * Split out from its parent because the two failures want opposite responses
58
+ * and the caller cannot tell them apart from a message. A transport or
59
+ * permission failure says nothing about whether the value exists — retrying it
60
+ * on another connection would hide a broken call behind a confident "no
61
+ * connection has it". An exhaustive miss is positive evidence about this
62
+ * connection specifically, and is the signal `prepare` uses to demote it and
63
+ * try the next candidate (flow-builder-sdk#744).
64
+ *
65
+ * `capped` flips that: a scan stopped at the record cap did NOT reach the end,
66
+ * so it is not evidence of absence and must not demote anything.
67
+ *
68
+ * The attributes carry the counts so a caller can report per-candidate results
69
+ * without re-parsing the sentence.
70
+ */
71
+ export class LookupExhausted extends ResolutionRefusal {
72
+ field;
73
+ collection;
74
+ by;
75
+ value;
76
+ scanned;
77
+ pages;
78
+ capped;
79
+ constructor(message, facts) {
80
+ super(message, "RESOLVE_NO_MATCH");
81
+ this.name = "LookupExhausted";
82
+ this.field = facts.field;
83
+ this.collection = facts.collection;
84
+ this.by = facts.by;
85
+ this.value = facts.value;
86
+ this.scanned = facts.scanned;
87
+ this.pages = facts.pages;
88
+ this.capped = facts.capped;
89
+ }
90
+ }
91
+ /**
92
+ * `channel:profile.email=dustin@example.com` -> `[field, by, value]`.
93
+ *
94
+ * Split on the FIRST colon and the FIRST `=`. Both halves can legitimately
95
+ * contain the other delimiter — `by` is a dotted path and a value can be
96
+ * anything a person typed — so a greedy split silently mis-parses real input.
97
+ */
98
+ export function parseResolveFlag(raw) {
99
+ const [field, rest] = splitOnce(raw, ":");
100
+ if (rest === undefined || !field) {
101
+ throw new ResolutionRefusal(`--resolve ${pyRepr(raw)}: expected <field>:<by>=<value>, ` +
102
+ "e.g. --resolve channel:profile.email=someone@example.com", "RESOLVE_BAD_FLAG");
103
+ }
104
+ const [by, value] = splitOnce(rest, "=");
105
+ if (value === undefined || !by) {
106
+ throw new ResolutionRefusal(`--resolve ${pyRepr(raw)}: expected <field>:<by>=<value>, ` +
107
+ `but found no '=' after the search field ${pyRepr(by || rest)}.`, "RESOLVE_BAD_FLAG");
108
+ }
109
+ return [field, by, value];
110
+ }
111
+ function specOf(specs, field) {
112
+ if (specs instanceof Map)
113
+ return specs.get(field);
114
+ const table = specs;
115
+ return Object.prototype.hasOwnProperty.call(table, field) ? table[field] : undefined;
116
+ }
117
+ /**
118
+ * Order the requests so a `dependsOn` prerequisite resolves first.
119
+ *
120
+ * 27 fields in the shipped library are not addressable until another field is
121
+ * known — Jira's issue type needs the project, Amazon Connect's phone number
122
+ * needs the instance. Resolving in the order the flags were typed leaves an
123
+ * unsubstituted `{placeholder}` in a URL, and the error the connector returns
124
+ * for that names neither field.
125
+ *
126
+ * A field requested twice keeps its first position and its LAST `by`/`value`,
127
+ * as the Python's dict did.
128
+ */
129
+ export function orderResolutions(requested, specs) {
130
+ const byField = new Map();
131
+ for (const request of requested)
132
+ byField.set(request[0], request);
133
+ const ordered = [];
134
+ const placed = new Set();
135
+ const place = (field, trail) => {
136
+ if (placed.has(field))
137
+ return;
138
+ if (trail.includes(field)) {
139
+ const cycle = [...trail, field].join(" -> ");
140
+ throw new ResolutionRefusal(`circular dependsOn among lookup fields: ${cycle}`, "RESOLVE_CYCLE");
141
+ }
142
+ const dependsOn = specOf(specs, field)?.dependsOn;
143
+ for (const prerequisite of Array.isArray(dependsOn) ? dependsOn : []) {
144
+ if (byField.has(prerequisite)) {
145
+ place(prerequisite, [...trail, field]);
146
+ }
147
+ else {
148
+ throw new ResolutionRefusal(`cannot resolve ${pyRepr(field)}: its collection is addressed through ` +
149
+ `${pyRepr(prerequisite)}, which has no --resolve of its own. Add ` +
150
+ `--resolve ${prerequisite}:<by>=<value> and re-run.`, "RESOLVE_MISSING_PREREQUISITE");
151
+ }
152
+ }
153
+ placed.add(field);
154
+ ordered.push(byField.get(field));
155
+ };
156
+ for (const [field] of requested)
157
+ place(field, []);
158
+ return ordered;
159
+ }
160
+ /**
161
+ * The records out of a `uip is resources run list` reply.
162
+ *
163
+ * The reply is `{"Result": "Success", "Code": ..., "Data": {"items": [...],
164
+ * "Pagination": {...}}}` — records live under `Data`, and `Result` is the
165
+ * STATUS STRING, not a container. Reading a top-level `items` finds nothing
166
+ * and reports "scanned 0 records" against a collection that is full, which is
167
+ * exactly what this did until an eval run walked it into the real CLI.
168
+ */
169
+ export function envelopeItems(payload) {
170
+ if (!isPlainObject(payload))
171
+ return [];
172
+ const data = payload.Data;
173
+ if (isPlainObject(data) && Array.isArray(data.items))
174
+ return data.items;
175
+ if (Array.isArray(payload.items))
176
+ return payload.items;
177
+ return [];
178
+ }
179
+ /** The `Pagination` block, from under `Data`. */
180
+ export function envelopePagination(payload) {
181
+ if (!isPlainObject(payload))
182
+ return {};
183
+ const data = payload.Data;
184
+ if (isPlainObject(data) && isPlainObject(data.Pagination))
185
+ return data.Pagination;
186
+ if (isPlainObject(payload.Pagination))
187
+ return payload.Pagination;
188
+ return {};
189
+ }
190
+ /**
191
+ * `HasMore` arrives as the STRING "false" as often as the boolean.
192
+ *
193
+ * `if payload["Pagination"]["HasMore"]` on the string "false" is True, which
194
+ * pages forever on a collection that just said it has no more.
195
+ */
196
+ export function truthy(value) {
197
+ if (typeof value === "string") {
198
+ const lowered = pyStrip(value).toLowerCase();
199
+ return lowered === "true" || lowered === "1" || lowered === "yes";
200
+ }
201
+ return pyTruthy(value);
202
+ }
203
+ /**
204
+ * The reviver that makes `JSON.parse` read a number the way `json.loads` did.
205
+ *
206
+ * Python kept every number as its text spelled it — `1.0` stayed a float,
207
+ * whose `str()` is `"1.0"`, and an integer stayed exact however many digits it
208
+ * had — so `str(got)` lost nothing on the way into `resolutions.json`.
209
+ * `JSON.parse` folds both cases: `1.0` parses to the integer `1`, and an
210
+ * integer above 2^53 is rounded to the nearest double, and the rounded number
211
+ * is what would have been recorded as the id. The reviver's third argument
212
+ * (`JSON.parse` source text access, present in every Node the package
213
+ * supports — `engines` says >= 22.6) is the spelling itself, so the two lossy
214
+ * cases are rewritten here as the string Python's `str()` would have produced.
215
+ * {@link matchRecord} and the recorded id only ever see that string, so
216
+ * nothing downstream can tell the difference — except that it is now right,
217
+ * and a typed `--resolve x:count=1.0` matches a JSON `1.0` as it did in Python.
218
+ */
219
+ export function reviveLikeJsonLoads(_key, value, context) {
220
+ if (typeof value !== "number")
221
+ return value;
222
+ const source = context?.source;
223
+ if (source === undefined) {
224
+ // An engine without source access. The integral float is undetectable
225
+ // here; a rounded integer is not, and must not be written as if exact.
226
+ if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
227
+ throw new ResolutionRefusal(`a record carries the integer ${String(value)}, which this JavaScript engine ` +
228
+ "rounded while parsing and cannot read exactly; run on Node >= 22.6.", "RESOLVE_IMPRECISE_NUMBER");
229
+ }
230
+ return value;
231
+ }
232
+ if (/[.eE]/.test(source)) {
233
+ // A float in Python. `pyStr` already renders a non-integral double as
234
+ // Python's repr; only an integral one loses its `.0` (or exponent form).
235
+ return Number.isInteger(value) ? pyFloatRepr(value) : value;
236
+ }
237
+ // An int in Python: exact whatever its length, and `-0` is plain `0`.
238
+ if (Object.is(value, -0))
239
+ return "0";
240
+ return Number.isSafeInteger(value) ? value : source;
241
+ }
242
+ /**
243
+ * The first JSON document in a listing's stdout, or `undefined`.
244
+ *
245
+ * The same search `uip/envelope.ts`'s `findJsonDocument` makes — the whole
246
+ * text first, then every line that begins with a bracket, since the banner the
247
+ * CLI prints ahead of its payload never starts a line with one. Repeated here
248
+ * because that module has no hook for a reviver and only the resolver needs
249
+ * one: nowhere else does a JSON number become an id that is written down.
250
+ */
251
+ function findJsonDocument(text, reviver) {
252
+ const trimmed = text.trim();
253
+ if (trimmed === "")
254
+ return undefined;
255
+ try {
256
+ return JSON.parse(trimmed, reviver);
257
+ }
258
+ catch (error) {
259
+ if (error instanceof ResolutionRefusal)
260
+ throw error;
261
+ // fall through to the line scan
262
+ }
263
+ const lines = text.split(/\r?\n/);
264
+ for (let i = 0; i < lines.length; i++) {
265
+ const head = lines[i].trimStart();
266
+ if (!head.startsWith("{") && !head.startsWith("["))
267
+ continue;
268
+ try {
269
+ return JSON.parse(lines.slice(i).join("\n"), reviver);
270
+ }
271
+ catch (error) {
272
+ if (error instanceof ResolutionRefusal)
273
+ throw error;
274
+ continue;
275
+ }
276
+ }
277
+ return undefined;
278
+ }
279
+ /**
280
+ * The plainly parsed envelope with only its RECORD values revived.
281
+ *
282
+ * {@link reviveLikeJsonLoads} is about ids: a record value that a flow will
283
+ * send back to the provider has to keep the spelling Python's `str()` gave it.
284
+ * The envelope AROUND the records is control data the code reads itself, and
285
+ * rewriting a number there changes behaviour rather than preserving it —
286
+ * `HasMore: 1.0` became the string `"1.0"`, which {@link truthy} reads as
287
+ * false, so paging stopped on a collection that had just said it has more.
288
+ * (Only that spelling flips: `true`, `1`, `false`, `"false"` and `0` all
289
+ * survive the reviver unchanged, which is why it went unnoticed.)
290
+ *
291
+ * So the records come from the revived parse and everything else from a plain
292
+ * one. A second `JSON.parse` of a page of listing output is cheap next to the
293
+ * `uip` call that produced it. A document that is not an envelope has no
294
+ * control fields to protect and stays fully revived.
295
+ */
296
+ function recordsRevived(plain, revived) {
297
+ if (!isPlainObject(plain) || !isPlainObject(revived))
298
+ return revived;
299
+ const data = plain.Data;
300
+ const revivedData = revived.Data;
301
+ if (isPlainObject(data) && Array.isArray(data.items) && isPlainObject(revivedData) && Array.isArray(revivedData.items)) {
302
+ return { ...plain, Data: { ...data, items: revivedData.items } };
303
+ }
304
+ // The flat shape {@link envelopeItems} falls back to.
305
+ if (Array.isArray(plain.items) && Array.isArray(revived.items))
306
+ return { ...plain, items: revived.items };
307
+ return plain;
308
+ }
309
+ /**
310
+ * The first JSON document in a listing's stdout, with its record values parsed
311
+ * as `json.loads` would have read them, or `undefined`.
312
+ */
313
+ export function parseListingDocument(text) {
314
+ const revived = findJsonDocument(text, reviveLikeJsonLoads);
315
+ if (revived === undefined)
316
+ return undefined;
317
+ const plain = findJsonDocument(text);
318
+ return plain === undefined ? revived : recordsRevived(plain, revived);
319
+ }
320
+ /**
321
+ * One `uip is resources run list` page, as the parsed envelope.
322
+ *
323
+ * `uip` prints a self-update failure to stderr before its real output, so
324
+ * stderr being non-empty is not evidence of anything. Judge the exit code and
325
+ * the payload.
326
+ */
327
+ export async function listPage(client, connectorKey, collection, connectionId, query) {
328
+ const result = await client.run(isResourcesRunList(connectorKey, collection, connectionId, query));
329
+ if (result.code !== 0) {
330
+ throw new ResolutionRefusal(`listing ${collection} failed: ${pyStrip(result.stderr) || "no output"}`, "RESOLVE_LISTING_FAILED");
331
+ }
332
+ const envelope = parseListingDocument(result.stdout);
333
+ if (!isPlainObject(envelope)) {
334
+ throw new ResolutionRefusal(`listing ${collection} returned output that is not JSON: ${notJsonReason(result.stdout)}`, "RESOLVE_LISTING_NOT_JSON");
335
+ }
336
+ return envelope;
337
+ }
338
+ /** `profile.email` out of a record, or `null` (Python's `None`) when any step is missing. */
339
+ export function readPath(record, dotted) {
340
+ let cursor = record;
341
+ for (const part of dotted.split(".")) {
342
+ if (!isPlainObject(cursor))
343
+ return null;
344
+ cursor = Object.prototype.hasOwnProperty.call(cursor, part) ? (cursor[part] ?? null) : null;
345
+ }
346
+ return cursor;
347
+ }
348
+ function resolvedEntries(resolved) {
349
+ return resolved instanceof Map ? [...resolved.entries()] : Object.entries(resolved);
350
+ }
351
+ /** Fill `{fields.project.key}` placeholders from already-resolved fields, in their order. */
352
+ export function substitute(template, resolved) {
353
+ let out = template;
354
+ for (const [field, value] of resolvedEntries(resolved)) {
355
+ out = out.replaceAll(`{${field}}`, pyStrAny(value));
356
+ }
357
+ return out;
358
+ }
359
+ /**
360
+ * True when the record's `by` path, coerced with `str()`, equals `value`.
361
+ *
362
+ * The coercion is the Python's: a boolean reads `True`, a missing path `None`,
363
+ * a nested object its `repr`. It is what lets `--resolve x:active=True` match a
364
+ * JSON `true`, and it is kept exactly.
365
+ */
366
+ export function matchRecord(record, by, value) {
367
+ return pyStrAny(readPath(record, by)) === value;
368
+ }
369
+ /**
370
+ * Page through a collection, yielding each page's records and returning where
371
+ * the scan stopped.
372
+ *
373
+ * Pages with exactly `nextPage=<token>` appended to the query. Guessing `page`
374
+ * / `pageToken` / `nextPageToken` is a documented way to loop forever on page
375
+ * one. Stops when the reply says there is no more, when it carries no token,
376
+ * at {@link MAX_SCANNED} records, or after {@link PAGE_GUARD} transitions.
377
+ */
378
+ export async function* scanCollection(client, connectorKey, collection, connectionId, query) {
379
+ let scanned = 0;
380
+ let pages = 0;
381
+ for (;;) {
382
+ const payload = await listPage(client, connectorKey, collection, connectionId, query);
383
+ const items = envelopeItems(payload);
384
+ scanned += items.length;
385
+ yield items;
386
+ const pagination = envelopePagination(payload);
387
+ const token = pagination.NextPageToken;
388
+ if (!truthy(pagination.HasMore) || !pyTruthy(token) || scanned >= MAX_SCANNED)
389
+ break;
390
+ pages += 1;
391
+ if (pages > PAGE_GUARD)
392
+ break;
393
+ // Exactly `nextPage`. Guessing `page` / `pageToken` / `nextPageToken`
394
+ // is a documented way to loop forever on page one.
395
+ query = query ? `${query}&nextPage=${pyStrAny(token)}` : `nextPage=${pyStrAny(token)}`;
396
+ }
397
+ return { scanned, pages, capped: scanned >= MAX_SCANNED || pages > PAGE_GUARD };
398
+ }
399
+ /**
400
+ * The connector-facing id for one lookup, or throw {@link ResolutionRefusal}.
401
+ *
402
+ * `resolved` carries the values of fields resolved earlier in this run, which
403
+ * is what makes a `dependsOn` path addressable. Parameters follow the Python
404
+ * `resolve_one(connector_key, spec, field, by, value, connection_id, resolved)`
405
+ * with the client in front.
406
+ *
407
+ * A collection read to its end with no match throws {@link LookupExhausted},
408
+ * the subclass the caller's retry keys on; every other failure is the parent.
409
+ */
410
+ export async function resolveOne(client, connectorKey, spec, field, by, value, connectionId, resolved = new Map()) {
411
+ const searchableBy = Array.isArray(spec.by) ? spec.by : [];
412
+ if (!searchableBy.includes(by)) {
413
+ const searchable = searchableBy.map(pyRepr).join(", ");
414
+ throw new ResolutionRefusal(`${field}: cannot search by ${pyRepr(by)}. Searchable by ${searchable || "(nothing declared)"}.`, "RESOLVE_BAD_BY");
415
+ }
416
+ const path = substitute(pyStrAny(pyOr(spec.path, "")), resolved);
417
+ if (path.includes("{")) {
418
+ const open = path.indexOf("{");
419
+ const close = path.indexOf("}");
420
+ const missing = close >= 0 ? path.slice(open + 1, close) : "?";
421
+ throw new ResolutionRefusal(`${field}: the collection path still contains {${missing}}. ` +
422
+ `Resolve ${pyRepr(missing)} first with --resolve ${missing}:<by>=<value>.`, "RESOLVE_UNSUBSTITUTED");
423
+ }
424
+ const [beforeQuery, afterQuery] = splitOnce(path, "?");
425
+ const collection = pyStripChars(beforeQuery, "/");
426
+ let query = afterQuery || undefined;
427
+ const send = pyStrAny(pyOr(spec.value, "id"));
428
+ // A server-side filter turns the whole scan into one request. Where the
429
+ // metadata declares one, use it; where it does not, page.
430
+ const filterPattern = spec.filterPattern;
431
+ if (pyTruthy(filterPattern)) {
432
+ const filtered = pyStrAny(filterPattern).replaceAll("{filter}", value);
433
+ query = query ? `${query}&${filtered}` : filtered;
434
+ }
435
+ const scan = scanCollection(client, connectorKey, collection, connectionId, query);
436
+ let step = await scan.next();
437
+ while (!step.done) {
438
+ for (const record of step.value) {
439
+ if (matchRecord(record, by, value)) {
440
+ const got = readPath(record, send);
441
+ if (got === null) {
442
+ throw new ResolutionRefusal(`${field}: matched a record on ${by}=${pyRepr(value)}, but it ` +
443
+ `carries no ${pyRepr(send)} to send.`, "RESOLVE_NO_VALUE");
444
+ }
445
+ return pyStrAny(got);
446
+ }
447
+ }
448
+ step = await scan.next();
449
+ }
450
+ const { scanned, pages, capped } = step.value;
451
+ // This states the FACT and stops; `prepare` owns the remedy, because
452
+ // `prepare` is what knows the other candidate connections.
453
+ //
454
+ // It deliberately carries NO "check the value" and NO `uip is resources`
455
+ // pointer. The old text led with both: it named only the value as suspect
456
+ // when the connection is equally suspect, and then sent the reader to a
457
+ // tenant crawl the skill explicitly rules out ("Tenant discovery is not a
458
+ // phase of either loop", references/CLI-LOOP.md). An agent followed that
459
+ // advice into exactly what it had been told not to do — 50 of 94 Bash calls
460
+ // hand-crawling Integration Service, 41 of them paging this collection, and
461
+ // the run hit its ceiling without producing a flow (flow-builder-sdk#744).
462
+ //
463
+ // The listing command is still offered, once, by the caller's terminal
464
+ // report — where every connection has been tried and seeing what IS there
465
+ // is finally the next step rather than a detour.
466
+ throw new LookupExhausted(`${field}: no record in ${pyRepr(collection)} has ${by}=${pyRepr(value)} ` +
467
+ `(scanned ${scanned} record(s) across ${pages + 1} page(s)` +
468
+ (capped ? `; STOPPED AT THE ${MAX_SCANNED}-record cap, so the value may be further in` : "; collection exhausted") +
469
+ `) on connection ${connectionId}.`, { field, collection, by, value, scanned, pages: pages + 1, capped });
470
+ }