@uipath/maestro-builder-sdk 6.47.2 → 6.48.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.
@@ -20,6 +20,8 @@ import { createConnectorEvent, createExternalTask, intsvcOutputVar } from './int
20
20
  import { createExtensionPayload, createTypedNode, rowDeclaration, typedNodeOutputVar, verbatimOutputRows } from './typed-node.js';
21
21
  import { connectorOutputVars, typedNodeDeclaredVars } from './declared-vars.js';
22
22
  import { defaultEntryPointId } from './entry-point-id.js';
23
+ import { parseXmlRanges } from './xml-ranges.js';
24
+ import { ownEntry } from '../core/own-entry.js';
23
25
  const require = createRequire(import.meta.url);
24
26
  const uipathDescriptor = require('./uipath-moddle.v1.json');
25
27
  const NS = {
@@ -181,6 +183,22 @@ export async function serialize(built, opts = {}) {
181
183
  * of the trade. Revisit if the graders learn to treat absence as interrupting.
182
184
  */
183
185
  export function finalizeBpmnXml(xml) {
186
+ return jsonInputBodiesAsCdata(outsideCdata(xml, normalizeMarkup));
187
+ }
188
+ /**
189
+ * Apply `edit` to the markup only — never inside a CDATA section or a comment.
190
+ *
191
+ * The passes below are patterns over the text, and a CDATA payload is author data: a
192
+ * JSON value holding the literal `&#60;` or `<bpmn:boundaryEvent>` must come out of a
193
+ * second `finalizeBpmnXml` (or `tidy`) byte for byte, not rewritten as markup.
194
+ */
195
+ function outsideCdata(xml, edit) {
196
+ return xml
197
+ .split(/(<!\[CDATA\[[\s\S]*?\]\]>|<!--[\s\S]*?-->)/)
198
+ .map((part, i) => (i % 2 === 1 ? part : edit(part)))
199
+ .join('');
200
+ }
201
+ function normalizeMarkup(xml) {
184
202
  return (xml
185
203
  .replace(/<bpmn:boundaryEvent\b(?![^>]*\bcancelActivity=)([^>]*?)(\/?)>/g, '<bpmn:boundaryEvent cancelActivity="true"$1$2>')
186
204
  // Same reason, other schema default: a PARALLEL multi-instance marker is
@@ -206,6 +224,81 @@ export function finalizeBpmnXml(xml) {
206
224
  .replace(/&#(?:34|x22);/g, '&quot;')
207
225
  .replace(/&#(?:39|x27);/g, '&apos;'));
208
226
  }
227
+ /**
228
+ * Rewrite every json `uipath:input` text body as CDATA — the one body shape the cloud
229
+ * engine reads.
230
+ *
231
+ * `PO.BpmnEngine`'s `UiPathExtensionsProvider` (`ExtractContextInputs`,
232
+ * `ExtractActivityInputs`) takes an input's value from an `XCData` child or a `value`
233
+ * attribute and filters out any row with neither. moddle writes a body as entity-escaped
234
+ * TEXT, which is neither, so the engine silently dropped every json input the SDK
235
+ * authored: a script task ran with no `args` (`vars is not defined`), a connector task
236
+ * sent no body. `check`, `compile` and `uip maestro bpmn validate` all read text and
237
+ * CDATA alike, so nothing caught it before the run (#1390). Studio Web writes CDATA.
238
+ *
239
+ * moddle has no CDATA option for a body property, so this rewrites its output — by
240
+ * element range (`xml-ranges.ts`), not a pattern over the document. It runs LAST in
241
+ * `finalizeBpmnXml`, after the markup passes, which skip CDATA (`outsideCdata`), so no
242
+ * pass reaches into a payload this writes. An input whose content is not plain text (already CDATA,
243
+ * as `tidy` may hand it, or empty) is left alone, so this is idempotent.
244
+ */
245
+ function jsonInputBodiesAsCdata(xml) {
246
+ const bodies = [];
247
+ const collect = (nodes) => {
248
+ for (const node of nodes) {
249
+ if (node.name === 'uipath:input' && node.attributes.type === 'json')
250
+ bodies.push(node);
251
+ else
252
+ collect(node.children);
253
+ }
254
+ };
255
+ collect(parseXmlRanges(xml));
256
+ let out = xml;
257
+ // Splice from the end so the earlier offsets stay valid.
258
+ for (const input of bodies.sort((a, b) => b.innerStart - a.innerStart)) {
259
+ const text = xml.slice(input.innerStart, input.innerEnd);
260
+ if (text === '' || text.includes('<'))
261
+ continue;
262
+ out = out.slice(0, input.innerStart) + cdata(input.attributes.name ?? '', decodeXmlText(text)) + out.slice(input.innerEnd);
263
+ }
264
+ return out;
265
+ }
266
+ const NAMED_ENTITY = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
267
+ /** Decode XML character data in one pass, so `&amp;lt;` stays the text `&lt;`. */
268
+ function decodeXmlText(text) {
269
+ return text.replace(/&(?:#(\d+)|#x([0-9a-fA-F]+)|(lt|gt|amp|quot|apos));/g, (_, dec, hex, named) => dec !== undefined
270
+ ? String.fromCodePoint(Number(dec))
271
+ : hex !== undefined
272
+ ? String.fromCodePoint(parseInt(hex, 16))
273
+ : ownEntry(NAMED_ENTITY, named));
274
+ }
275
+ /**
276
+ * ONE CDATA section holding `text`'s value.
277
+ *
278
+ * A `]]>` cannot sit in a CDATA section, and splitting it across two is no answer: the
279
+ * engine reads the FIRST `XCData` child only (`OfType<XCData>().FirstOrDefault()`), so it
280
+ * would get the JSON cut off at the split. In valid JSON a `]]>` can only sit inside a
281
+ * string (`>` is not JSON outside one), so its `>` is written as the escape `\u003e` —
282
+ * different bytes, the same value. A body that is not JSON has no such escape and could
283
+ * only reach the engine truncated, so it is refused.
284
+ */
285
+ function cdata(name, text) {
286
+ if (!text.includes(']]>'))
287
+ return `<![CDATA[${text}]]>`;
288
+ let value;
289
+ try {
290
+ value = JSON.parse(text);
291
+ }
292
+ catch {
293
+ throw new Error(`json input "${name}" contains "]]>" and is not valid JSON, so it cannot be written as the single CDATA section the engine reads.`);
294
+ }
295
+ const escaped = text.replaceAll(']]>', ']]\\u003e');
296
+ // Holds by the argument above; checked because a wrong payload here would be silent.
297
+ if (JSON.stringify(JSON.parse(escaped)) !== JSON.stringify(value)) {
298
+ throw new Error(`json input "${name}": escaping "]]>" changed its value.`);
299
+ }
300
+ return `<![CDATA[${escaped}]]>`;
301
+ }
209
302
  export function processId(id) {
210
303
  return /^[A-Za-z_][\w.-]*$/.test(id) ? id : 'Process_1';
211
304
  }
package/dist/check.js CHANGED
@@ -5,11 +5,11 @@
5
5
  * duplicate step names, references to undeclared variables/steps, empty branch
6
6
  * arms, and missing terminals.
7
7
  */
8
- import { Expr, SCHEDULE_PRESETS, parseIxpProjectType, parallelFork } from './flow-sdk.js';
8
+ import { Expr, SCHEDULE_PRESETS, parallelFork } from './flow-sdk.js';
9
9
  import { FlowNode } from './core/node-classes.js';
10
10
  import { planContributionBindings } from './core/contribution-bindings.js';
11
11
  import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
12
- import { bundledActionNodeType, bundledContainerDefinition, bundledDefinition, perCallDefinition, resolveContributionDefinition } from './serialize.js';
12
+ import { bundledActionNodeType, bundledContainerDefinition, bundledDefinition, resolveContributionDefinition } from './serialize.js';
13
13
  import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
14
14
  import { checkCookedEscapes } from './flow-expr-check.js';
15
15
  import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
@@ -528,8 +528,6 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
528
528
  outputReaders.set(stepName, (field, rest, at, d) => rule(stepName, field, rest, inputs, at, d));
529
529
  }
530
530
  }
531
- if (s.spec.kind === 'ixpExtract')
532
- checkIxpExtract(s.name, s.spec.inputs, diags);
533
531
  if (s.spec.kind === 'waitEvent') {
534
532
  const stepName = s.name;
535
533
  const push = (level) => (code, message, suggestion) => diags.push({
@@ -2194,76 +2192,6 @@ const OUTPUT_READ_RULES = {
2194
2192
  + 'its own false-positive sweep — flow-builder-sdk#855.',
2195
2193
  },
2196
2194
  };
2197
- /**
2198
- * The five forbidden inputs on an IxP node. They belonged to a prior schema and
2199
- * v1's own authoring reference calls them "the most common training-data-recall
2200
- * mistake" — the current node declares none of them, so each is an input the
2201
- * definition does not have.
2202
- *
2203
- * They are not reachable through `ixpExtract()`'s typed surface, which is the
2204
- * point of having one; this check exists for the JS caller and for a
2205
- * hand-written spec object, where TypeScript is not in the way.
2206
- */
2207
- const IXP_FORBIDDEN_INPUTS = ['digitizationMode', 'documentTaxonomy', 'attachmentId', 'fileName', 'mimeType'];
2208
- /**
2209
- * `ixpExtract(...)`-specific checks.
2210
- *
2211
- * All four identity fields are load-bearing on the tenant and none of them can
2212
- * be derived, so a missing one is an error rather than a warning: the node type
2213
- * is per-project, and the definition's own `inputDefinition` marks `modelName`
2214
- * and `folderKey` required (`minLength: 1`) beside `fileRef` — which is why
2215
- * `flow validate`'s `ixp-node` validator rejects a node with an empty one.
2216
- */
2217
- function checkIxpExtract(step, inputs, diags) {
2218
- const at = (code, message, suggestion) => {
2219
- diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
2220
- };
2221
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
2222
- const project = String(inputs?.project ?? '').trim();
2223
- if (blank(project)) {
2224
- at('IXP_NO_PROJECT', `IxP step "${step}" has no \`project\`. That is the extractor's whole node type, and it carries the `
2225
- + `project version id and folder key the platform resolves the deployment by. Copy it from the registry `
2226
- + `— never construct it: three different spellings of its tail exist in deployed flows.`, `project: 'uipath.ixp.<slug>.<projectVersionId>-<folderKey>' // uip maestro flow registry pull --force && uip maestro flow registry search -f "type:startsWith=uipath.ixp"`);
2227
- }
2228
- else if (!project.startsWith('uipath.ixp.')) {
2229
- at('IXP_PROJECT_NOT_NODE_TYPE', `IxP step "${step}"'s \`project\` is ${JSON.stringify(inputs.project)}, which is not a \`uipath.ixp.*\` node `
2230
- + `type. This field takes the type verbatim from \`registry search\`, not the project's name or id — the `
2231
- + `name goes in \`name\` and the slug in \`modelName\`.`);
2232
- }
2233
- else if (parseIxpProjectType(project) === undefined) {
2234
- at('IXP_PROJECT_TAIL_UNPARSEABLE', `IxP step "${step}"'s \`project\` ends in ${JSON.stringify(project.slice(project.lastIndexOf('.') + 1))}, which is `
2235
- + `not the \`<projectVersionId>-<folderKey>\` pair of uuids the current registry serves. Two older spellings `
2236
- + `exist in the corpus — a sanitized folder path, and the literal string "undefined" — and neither resolves `
2237
- + `today. Re-read the type from \`registry search\` after \`registry pull --force\`.`);
2238
- }
2239
- if (blank(inputs?.modelName)) {
2240
- at('IXP_NO_MODEL_NAME', `IxP step "${step}" has no \`modelName\`. The definition marks it required (\`minLength: 1\`) and the `
2241
- + `platform's \`ixp-node\` validator refuses an empty one — it is the project's own name as the tenant `
2242
- + `spells it, which the node type's slug has lower-cased and de-punctuated beyond recovery.`, `modelName: 'invoiceixp-cef0d447-ixp' // registry get → inputDefaults.modelName`);
2243
- }
2244
- if (blank(inputs?.name)) {
2245
- at('IXP_NO_NAME', `IxP step "${step}" has no \`name\`. It is the deployment's display name — the \`ixpDeployment\` binding's `
2246
- + `\`name\`, which is how the deployed flow finds the project.`, `name: 'InvoiceIXP' // registry get → inputDefaults.model.modelDisplayName`);
2247
- }
2248
- if (blank(inputs?.folderName)) {
2249
- at('IXP_NO_FOLDER', `IxP step "${step}" has no \`folderName\`. IxP deployments are folder-scoped, and this is the binding's `
2250
- + `\`folderPath\` default — the folder the runtime looks in.`, `folderName: 'Shared' // registry get → inputDefaults.folderName`);
2251
- }
2252
- const fileRef = inputs?.fileRef;
2253
- if (fileRef === undefined || fileRef === null
2254
- || (typeof fileRef === 'string' && fileRef.trim() === '')) {
2255
- at('IXP_NO_FILE', `IxP step "${step}" has no \`fileRef\`. It is the node's only required INPUT (\`inputDefinition.required\`) `
2256
- + `and it takes a whole Flow Attachment object — normally a \`file\`-typed flow input.`, `fileRef: out('start', 'invoiceFile') // .input({ invoiceFile: types.file })`);
2257
- }
2258
- for (const f of IXP_FORBIDDEN_INPUTS) {
2259
- if (inputs?.[f] !== undefined) {
2260
- at('IXP_FORBIDDEN_INPUT', `IxP step "${step}" sets \`${f}\`, which the current node does not declare — it belonged to a prior `
2261
- + `schema. ${f === 'documentTaxonomy' ? 'The taxonomy travels in the resolved `model` blob.'
2262
- : f === 'digitizationMode' ? 'The serializer defaults it to `fileUpload` internally.'
2263
- : 'It is derived from `fileRef` upstream.'}`);
2264
- }
2265
- }
2266
- }
2267
2195
  /** Best guess at what the author meant, for a `SCHEDULE_NOT_ISO` suggestion. */
2268
2196
  function nearestInterval(every) {
2269
2197
  // By far the likeliest slip: a correct duration missing the "R/" repeat marker.
@@ -3093,30 +3021,11 @@ placed, carried) {
3093
3021
  const bundledDef = bundledType
3094
3022
  ? bundledDefinition(bundledType, pinned) ?? (pinned !== undefined ? carried?.(bundledType, pinned) : undefined)
3095
3023
  : undefined;
3096
- // A per-tenant family: ask the definition serialize synthesizes for it (#1040).
3097
- let perCallDef;
3098
- if (!bundledType) {
3099
- try {
3100
- perCallDef = perCallDefinition(spec);
3101
- }
3102
- catch {
3103
- // The family's own checks report a spec its builder cannot lower.
3104
- return;
3105
- }
3106
- }
3107
- const def = bundledType ? bundledDef : perCallDef;
3108
- const noPort = def !== undefined
3109
- ? !definitionHasErrorPort(def)
3024
+ // Every per-tenant family is registered and answers above (its errorPort, or
3025
+ // a carried definition); what is left is bundled, or has no definition here.
3026
+ const noPort = bundledDef !== undefined
3027
+ ? !definitionHasErrorPort(bundledDef)
3110
3028
  : !bundledType && !NO_DEFINITION_ERROR_PORT_KINDS.has(spec.kind);
3111
- if (noPort && perCallDef !== undefined) {
3112
- diags.push({
3113
- level: 'error',
3114
- code: 'ONERROR_NO_ERROR_PORT',
3115
- message: errorPortRefusal(step.name, perCallDef, { bundled: true }),
3116
- step: step.name,
3117
- });
3118
- return;
3119
- }
3120
3029
  if (noPort) {
3121
3030
  diags.push({
3122
3031
  level: 'error',
@@ -257,98 +257,6 @@ export interface InOutField extends FormFieldSettings {
257
257
  required?: boolean;
258
258
  }
259
259
  export type FormField = ShownField | AskedField | InOutField;
260
- /**
261
- * Split an extractor's node type into the two GUIDs its tail packs.
262
- *
263
- * `uipath.ixp.<projectSlug>.<projectVersionId>-<folderKey>` — the version id is
264
- * the `ixpDeployment` binding's `resourceKey` and the node's `model.id`; the
265
- * folder key is `inputs.folderKey`. Returns `undefined` for a tail that is not
266
- * exactly two uuids joined by `-`, which is how the two older spellings (a
267
- * sanitized folder path, and the literal string `undefined`) are refused rather
268
- * than mis-split.
269
- *
270
- * @internal Public for compatibility; not an authoring factory.
271
- */
272
- export declare function parseIxpProjectType(nodeType: string): {
273
- slug: string;
274
- versionId: string;
275
- folderKey: string;
276
- } | undefined;
277
- export interface IxpExtractInputs {
278
- /**
279
- * The extractor's node type, verbatim from
280
- * `uip maestro flow registry search -f "type:startsWith=uipath.ixp"` — e.g.
281
- * `'uipath.ixp.invoiceixp-cef0d447-ixp.ff973488-5d89-8035-86ac-f980b4be6deb-c4359cde-55f0-4f0e-9322-c6cdce74ab4c'`.
282
- *
283
- * **Run `uip maestro flow registry pull --force` first.** `registry search`
284
- * and `registry get` answer from a local cache (`~/.uipath/nodes/`), so
285
- * without a pull you are reading whenever you last synced.
286
- *
287
- * Two GUIDs are packed in that tail — the project **version** id and the
288
- * **folder** key — and both are read back out of it here, which is why
289
- * nothing else has to be passed twice.
290
- */
291
- project: string;
292
- /**
293
- * The project's own name, as the tenant spells it — the slug, e.g.
294
- * `'idp-benchmark---invoices-c735405a-ixp'`. It is
295
- * `inputDefaults.modelName` in `registry get`, and the node carries it as
296
- * both `modelName` and `projectName` (one value, two fields — the designer
297
- * shows it in two disabled boxes).
298
- *
299
- * It cannot be recovered from {@link IxpExtractInputs.project}: the node type's slug is
300
- * lower-cased and de-punctuated (`---` collapses to `-`), which does not
301
- * invert.
302
- */
303
- modelName: string;
304
- /**
305
- * The deployment's display name, e.g. `'InvoiceIXP'` —
306
- * `inputDefaults.model.modelDisplayName`. It becomes the node's
307
- * `model.modelDisplayName` and the `ixpDeployment` binding's `name`.
308
- */
309
- name: string;
310
- /**
311
- * The folder the project is published in, e.g. `'Shared'` or
312
- * `'Shared/uipath-maestro-flow/BillingDispute'` —
313
- * `inputDefaults.folderName`. It becomes the binding's `folderPath` default
314
- * and the definition's own `description`.
315
- */
316
- folderName: string;
317
- /**
318
- * The document to extract from — a whole **Flow Attachment** object, the same
319
- * shape and the same two ways of referencing it as
320
- * {@link SummarizeInputs.attachment}:
321
- *
322
- * ```ts
323
- * flow('InvoiceIntake')
324
- * .input({ invoiceFile: types.file })
325
- * .step('extract', ixpExtract({ …, fileRef: out('start', 'invoiceFile') }))
326
- * ```
327
- *
328
- * It is the node's only authored input, and the definition marks it
329
- * `required` (with `modelName` and `folderKey`, which this factory fills in).
330
- */
331
- fileRef: Expr;
332
- /** Pages to process, e.g. `'1-5'`. Empty (the default) means the whole document. */
333
- pageRange?: string;
334
- /** Pin a published version tag. Empty (the default) means the latest. */
335
- versionTag?: string;
336
- /**
337
- * The folder's key CHAIN, when the project lives in a nested folder —
338
- * `inputDefaults.model.folderPath`, a dot-joined path of ancestor folder keys
339
- * (`<root>.<parent>.<folder>`). Defaults to the folder key alone, which is
340
- * what the platform writes for a project published straight into `Shared`.
341
- */
342
- folderPath?: string;
343
- /**
344
- * A free-text note on the node. Off by default, deliberately: the definition
345
- * does not declare it (so setting it draws a "sets an input its type does not
346
- * declare" warning unless the emitted definition is widened in the same
347
- * write, which this does), and it is empty in 114 of the 117 deployed nodes
348
- * that carry it at all.
349
- */
350
- description?: string;
351
- }
352
260
  /**
353
261
  * Where a process resource lives: deployed to an Orchestrator folder
354
262
  * (`'published'`, the default) or a sibling project of the SAME solution
@@ -914,10 +822,6 @@ export interface ContextIndexRef extends AgentResourcePlacement {
914
822
  * whole body rather than an inputs payload.
915
823
  */
916
824
  export interface FlowActionSpecs {
917
- /** The spec {@link ixpExtract} builds. */
918
- ixpExtract: {
919
- inputs: IxpExtractInputs;
920
- };
921
825
  /** The spec {@link connector} builds. */
922
826
  connector: {
923
827
  /** The connector key, e.g. `'uipath-salesforce-slack'`. */
@@ -977,61 +881,6 @@ export declare function builtinActionSpec<S extends object>(spec: S): S;
977
881
  * @returns The same spec, as a `FlowTrigger` instance.
978
882
  */
979
883
  export declare function builtinTriggerSpec<S extends object>(spec: S): S;
980
- /**
981
- * Extract fields from a document with a published IxP project — the platform's
982
- * **Extract** node (`uipath.ixp.*`, service contract `IXP.Extraction`).
983
- *
984
- * @remarks
985
- * **The result's shape is three levels deep and PascalCase.** A field lands at
986
- * `out('<step>', 'ExtractionResult.ResultsDocument.Fields')` — an array of
987
- * `{ FieldId, FieldName, FieldType, IsMissing, Values[], Confidence }`, looked
988
- * up by `FieldName`. `Fields[]` is under `ResultsDocument`, not directly under
989
- * `ExtractionResult`, and there is no `result` wrapper: the platform's
990
- * serializer strips it before the value reaches the variable.
991
- *
992
- * **A local run cannot extract anything** — extraction needs a real document on
993
- * the IxP service. An offline run returns the baked
994
- * fixture shape, and only a platform run reaches the model. So a green local
995
- * rung proves the wiring and the definition, never that a document was read.
996
- *
997
- * **`fileRef` is emitted as a PLAIN `=js:` string, not the expression envelope
998
- * the rest of the file uses** — the one place this family diverges from the
999
- * format's general spelling, and the contract this node owes the platform. The
1000
- * product's own `ixp-node` validator tests `typeof fileRef !== 'string'` and
1001
- * refuses the object form (*"inputs.fileRef must be a `=js:$vars.…`
1002
- * expression"*), the node's registry manifest declares
1003
- * `{type:'string', minLength:1}`, and so do all 150 v1-authored instances in the
1004
- * deployed corpus that carry the field — at file versions 1.6, 1.8 and 1.9 alike,
1005
- * with zero envelopes among them. The RUNTIME
1006
- * accepts both, so only `uip maestro flow validate` sees a wrong one; the
1007
- * emitter's exemption lives in `migrate.ts` (`UNWRAPPED_INPUTS`).
1008
- * See the IxP node reference (`website/reference/ixp.md`) §3.
1009
- *
1010
- * @example
1011
- * **Extract an invoice total and read it out of the nested result**
1012
- * ```ts
1013
- * flow('InvoiceIntake')
1014
- * .input({ invoiceFile: types.file })
1015
- * .output({ total: types.string })
1016
- * .step('extract', ixpExtract({
1017
- * project: 'uipath.ixp.invoiceixp-cef0d447-ixp.ff973488-5d89-8035-86ac-f980b4be6deb-c4359cde-55f0-4f0e-9322-c6cdce74ab4c',
1018
- * modelName: 'invoiceixp-cef0d447-ixp',
1019
- * name: 'InvoiceIXP',
1020
- * folderName: 'Shared',
1021
- * fileRef: out('start', 'invoiceFile'),
1022
- * }))
1023
- * .step('readTotal', script({ code: `
1024
- * const fields = $vars.extract.output.ExtractionResult.ResultsDocument.Fields || [];
1025
- * return { total: fields.find(f => f.FieldName === 'invoiceTotal')?.Values?.[0] };
1026
- * ` }))
1027
- * .return({ total: out('readTotal', 'total') })
1028
- * ```
1029
- *
1030
- * @param inputs - The published IxP `project` and `modelName`, and the document
1031
- * to read (`fileRef`).
1032
- * @returns An action spec to pass to `.step(name, …)`.
1033
- */
1034
- export declare function ixpExtract(inputs: IxpExtractInputs): ActionSpec;
1035
884
  /**
1036
885
  * Declare an Integration Service connector action — the typed form, where a
1037
886
  * generated descriptor supplies the nodeType and statically-checked input types:
@@ -1,44 +1,5 @@
1
1
  import { splitConnectorNodeType, isTriggerDescriptor, } from './connectors.js';
2
2
  import { FlowAction, FlowTrigger } from './node-classes.js';
3
- // ─── Document extraction (uipath.ixp.*) ──────────────────────────────────────
4
- //
5
- // The platform's *Extract* node (that is its `display.canvasLabel`): it hands a
6
- // document to a published IxP — Intelligent eXtraction Platform — project and
7
- // reads the trained model's fields back. Every extractor on a tenant is its own
8
- // node type, `uipath.ixp.<projectSlug>.<projectVersionId>-<folderKey>`, so the
9
- // four identity fields below come from the tenant and are named exactly as the
10
- // node names them.
11
- //
12
- // The type is RESOLVED, never constructed: three different spellings of that
13
- // tail exist in the deployed corpus (the sanitized folder path, the literal
14
- // string `undefined`, and today's version-id + folder-key pair), so copy the
15
- // one `registry search` prints.
16
- /**
17
- * Split an extractor's node type into the two GUIDs its tail packs.
18
- *
19
- * `uipath.ixp.<projectSlug>.<projectVersionId>-<folderKey>` — the version id is
20
- * the `ixpDeployment` binding's `resourceKey` and the node's `model.id`; the
21
- * folder key is `inputs.folderKey`. Returns `undefined` for a tail that is not
22
- * exactly two uuids joined by `-`, which is how the two older spellings (a
23
- * sanitized folder path, and the literal string `undefined`) are refused rather
24
- * than mis-split.
25
- *
26
- * @internal Public for compatibility; not an authoring factory.
27
- */
28
- export function parseIxpProjectType(nodeType) {
29
- const rest = nodeType.startsWith('uipath.ixp.') ? nodeType.slice('uipath.ixp.'.length) : undefined;
30
- if (!rest)
31
- return undefined;
32
- const dot = rest.lastIndexOf('.');
33
- if (dot <= 0)
34
- return undefined;
35
- const slug = rest.slice(0, dot);
36
- const tail = rest.slice(dot + 1);
37
- const m = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(tail);
38
- if (!m)
39
- return undefined;
40
- return { slug, versionId: m[1], folderKey: m[2] };
41
- }
42
3
  // ── Built-in spec adapters ──────────────────────────────────────────────────
43
4
  // Every factory result IS a FlowAction/FlowTrigger instance (design §4.4 pass
44
5
  // 1), so `instanceof` can tell an SDK-owned construct from a raw literal. The
@@ -88,63 +49,6 @@ export function builtinActionSpec(spec) {
88
49
  export function builtinTriggerSpec(spec) {
89
50
  return new BuiltinTrigger(spec);
90
51
  }
91
- /**
92
- * Extract fields from a document with a published IxP project — the platform's
93
- * **Extract** node (`uipath.ixp.*`, service contract `IXP.Extraction`).
94
- *
95
- * @remarks
96
- * **The result's shape is three levels deep and PascalCase.** A field lands at
97
- * `out('<step>', 'ExtractionResult.ResultsDocument.Fields')` — an array of
98
- * `{ FieldId, FieldName, FieldType, IsMissing, Values[], Confidence }`, looked
99
- * up by `FieldName`. `Fields[]` is under `ResultsDocument`, not directly under
100
- * `ExtractionResult`, and there is no `result` wrapper: the platform's
101
- * serializer strips it before the value reaches the variable.
102
- *
103
- * **A local run cannot extract anything** — extraction needs a real document on
104
- * the IxP service. An offline run returns the baked
105
- * fixture shape, and only a platform run reaches the model. So a green local
106
- * rung proves the wiring and the definition, never that a document was read.
107
- *
108
- * **`fileRef` is emitted as a PLAIN `=js:` string, not the expression envelope
109
- * the rest of the file uses** — the one place this family diverges from the
110
- * format's general spelling, and the contract this node owes the platform. The
111
- * product's own `ixp-node` validator tests `typeof fileRef !== 'string'` and
112
- * refuses the object form (*"inputs.fileRef must be a `=js:$vars.…`
113
- * expression"*), the node's registry manifest declares
114
- * `{type:'string', minLength:1}`, and so do all 150 v1-authored instances in the
115
- * deployed corpus that carry the field — at file versions 1.6, 1.8 and 1.9 alike,
116
- * with zero envelopes among them. The RUNTIME
117
- * accepts both, so only `uip maestro flow validate` sees a wrong one; the
118
- * emitter's exemption lives in `migrate.ts` (`UNWRAPPED_INPUTS`).
119
- * See the IxP node reference (`website/reference/ixp.md`) §3.
120
- *
121
- * @example
122
- * **Extract an invoice total and read it out of the nested result**
123
- * ```ts
124
- * flow('InvoiceIntake')
125
- * .input({ invoiceFile: types.file })
126
- * .output({ total: types.string })
127
- * .step('extract', ixpExtract({
128
- * project: 'uipath.ixp.invoiceixp-cef0d447-ixp.ff973488-5d89-8035-86ac-f980b4be6deb-c4359cde-55f0-4f0e-9322-c6cdce74ab4c',
129
- * modelName: 'invoiceixp-cef0d447-ixp',
130
- * name: 'InvoiceIXP',
131
- * folderName: 'Shared',
132
- * fileRef: out('start', 'invoiceFile'),
133
- * }))
134
- * .step('readTotal', script({ code: `
135
- * const fields = $vars.extract.output.ExtractionResult.ResultsDocument.Fields || [];
136
- * return { total: fields.find(f => f.FieldName === 'invoiceTotal')?.Values?.[0] };
137
- * ` }))
138
- * .return({ total: out('readTotal', 'total') })
139
- * ```
140
- *
141
- * @param inputs - The published IxP `project` and `modelName`, and the document
142
- * to read (`fileRef`).
143
- * @returns An action spec to pass to `.step(name, …)`.
144
- */
145
- export function ixpExtract(inputs) {
146
- return builtinActionSpec({ kind: 'ixpExtract', inputs });
147
- }
148
52
  export function connector(a, b, c, d) {
149
53
  if (typeof a === 'string') {
150
54
  const opts = (d ?? {});
@@ -52,7 +52,8 @@ export declare class NodeTypeRegistry<Ctx extends ArtifactContexts> {
52
52
  typesOfKind(kind: string): readonly Registered<Ctx>[];
53
53
  /**
54
54
  * The node type for a type name in the file — the exact name, else the type
55
- * whose `typeNamePrefix` it starts with — or undefined when no type owns it.
55
+ * whose `typeNamePrefix` it starts with (and whose `typeNameMatches`, if any,
56
+ * accepts it) — or undefined when no type owns it.
56
57
  * The bare prefix counts: it is what an empty `key` builds, and the old lift
57
58
  * read it as its factory, so it still does.
58
59
  */
@@ -1,5 +1,9 @@
1
1
  import { asVersioned } from './versioned.js';
2
2
  import { ownEntry } from '../own-entry.js';
3
+ /** Whether a prefix entry owns `typeName`: under its prefix, and accepted by its narrowing, if it has one. */
4
+ function owns(p, typeName) {
5
+ return typeName.startsWith(p.prefix) && (p.matches?.(typeName) ?? true);
6
+ }
3
7
  export class NodeTypeRegistry {
4
8
  artifact;
5
9
  families;
@@ -44,15 +48,18 @@ export class NodeTypeRegistry {
44
48
  const overlap = this.#byPrefix.find((p) => p.prefix.startsWith(prefix) || prefix.startsWith(p.prefix));
45
49
  if (overlap)
46
50
  problems.push(`${where}: prefix "${prefix}" overlaps "${overlap.prefix}" (${placeOf(overlap.entry)})`);
47
- const shadowed = [...this.#byTypeName.keys()].find((n) => n.startsWith(prefix));
51
+ const shadowed = [...this.#byTypeName.keys()].find((n) => n.startsWith(prefix) && (type.typeNameMatches?.(n) ?? true));
48
52
  if (shadowed)
49
53
  problems.push(`${where}: prefix "${prefix}" would also own the registered type "${shadowed}"`);
50
54
  }
51
55
  else {
52
- const owner = this.#byPrefix.find((p) => type.typeName.startsWith(p.prefix));
56
+ const owner = this.#byPrefix.find((p) => owns(p, type.typeName));
53
57
  if (owner)
54
58
  problems.push(`${where}: type name "${type.typeName}" falls under the prefix "${owner.prefix}" (${placeOf(owner.entry)})`);
55
59
  }
60
+ if (type.typeNameMatches !== undefined && type.typeNamePrefix === undefined) {
61
+ problems.push(`${where}: typeNameMatches narrows a typeNamePrefix, and the type declares none`);
62
+ }
56
63
  const typeOwner = this.#byTypeName.get(type.typeName);
57
64
  if (typeOwner) {
58
65
  problems.push(`type name "${type.typeName}" is registered twice (${placeOf(typeOwner)} and ${where}); one VersionedNodeType owns every version of a type`);
@@ -70,7 +77,7 @@ export class NodeTypeRegistry {
70
77
  this.#byKind.set(type.kind, [...(sharing ?? []), entry]);
71
78
  this.#byTypeName.set(type.typeName, entry);
72
79
  if (type.typeNamePrefix !== undefined) {
73
- this.#byPrefix.push({ prefix: type.typeNamePrefix, entry });
80
+ this.#byPrefix.push({ prefix: type.typeNamePrefix, matches: type.typeNameMatches?.bind(type), entry });
74
81
  }
75
82
  }
76
83
  }
@@ -102,7 +109,8 @@ export class NodeTypeRegistry {
102
109
  }
103
110
  /**
104
111
  * The node type for a type name in the file — the exact name, else the type
105
- * whose `typeNamePrefix` it starts with — or undefined when no type owns it.
112
+ * whose `typeNamePrefix` it starts with (and whose `typeNameMatches`, if any,
113
+ * accepts it) — or undefined when no type owns it.
106
114
  * The bare prefix counts: it is what an empty `key` builds, and the old lift
107
115
  * read it as its factory, so it still does.
108
116
  */
@@ -110,7 +118,7 @@ export class NodeTypeRegistry {
110
118
  const exact = this.#byTypeName.get(typeName);
111
119
  if (exact)
112
120
  return exact;
113
- return this.#byPrefix.find((p) => typeName.startsWith(p.prefix))?.entry;
121
+ return this.#byPrefix.find((p) => owns(p, typeName))?.entry;
114
122
  }
115
123
  /** Every registered kind, sorted. */
116
124
  kinds() {
@@ -25,6 +25,14 @@ Impl extends NodeType<Spec, Serialized, Ctx> = NodeType<Spec, Serialized, Ctx>>
25
25
  * per-tenant: no definition could be bundled for a name the SDK cannot know.
26
26
  */
27
27
  readonly typeNamePrefix?: string;
28
+ /**
29
+ * Narrows {@link typeNamePrefix}: the type owns a name under the prefix only
30
+ * when this accepts it. For a family whose names share a stem with exact names
31
+ * of other types — an IxP extractor is `uipath.ixp.<slug>.<versionId>-<folderKey>`,
32
+ * beside `uipath.ixp.extract-document-builder` — the registry then lets an
33
+ * exact name sit under the prefix as long as this rejects it.
34
+ */
35
+ typeNameMatches?(typeName: string): boolean;
28
36
  readonly factory: string;
29
37
  /** The version used when the author names none. Changing it is a breaking change. */
30
38
  readonly defaultVersion: string;
package/dist/decompile.js CHANGED
@@ -38,7 +38,6 @@ import { commentText } from './core/comment-text.js';
38
38
  import { tsKey as key, tsLiteral, tsNumber, tsObject as objLit, tsOptions as opts, tsText } from './core/ts-literal.js';
39
39
  import { wireExprBody } from './core/wire-expr.js';
40
40
  import { hasExits } from './core/step-ports.js';
41
- import { parseIxpProjectType } from './core/actions.js';
42
41
  import { readFlowGlobals } from './flow-variables.js';
43
42
  import { outputBackingGlobal, outputBackingName } from './flow-output-backing.js';
44
43
  import { mapJsCode } from './core/js-code-spans.js';
@@ -802,36 +801,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
802
801
  if (sub !== undefined)
803
802
  return `${imp.use('waitForEvent')}(${sub})`;
804
803
  }
805
- // A published IxP extractor: `uipath.ixp.<slug>.<versionId>-<folderKey>`.
806
- // The document-processing pair (`uipath.ixp.extract-document-builder`,
807
- // `uipath.document.classify`) are their own families, handled above and
808
- // excluded here by the parse — their tails are not the GUID pair.
809
- if (node.type.startsWith('uipath.ixp.') && parseIxpProjectType(node.type) !== undefined) {
810
- const i = (node.inputs ?? {});
811
- const plain = (k) => {
812
- const v = unwrapSource(i[k]);
813
- return v === undefined || v === '' ? undefined : v;
814
- };
815
- const fr = unwrapSource(i.fileRef) ?? '';
816
- // The DEPLOYMENT's display name is nested in `inputs.model`, not at the
817
- // top of `inputs` — the two are different values (the model block's
818
- // `modelDisplayName` is 'InvoiceIXP'; `modelName` is the slug), so
819
- // reading the wrong one recovers the slug as the name.
820
- const model = (i.model ?? {});
821
- const nested = (k) => typeof model[k] === 'string' && model[k] !== '' ? model[k] : undefined;
822
- return `${imp.use('ixpExtract')}(${opts([
823
- ['project', tsLiteral(node.type)],
824
- // `modelName`/`projectName` are one value in two fields; either recovers it.
825
- ['modelName', tsLiteral(plain('modelName') ?? plain('projectName') ?? nested('modelName') ?? '')],
826
- ['name', tsLiteral(nested('modelDisplayName') ?? plain('modelName') ?? '')],
827
- ['folderName', tsLiteral(plain('folderName') ?? nested('folderName') ?? '')],
828
- ['fileRef', fr.startsWith('=js:') ? emitBareExpr(fr.slice(4), imp, inputNames) : tsLiteral(fr)],
829
- ['pageRange', plain('pageRange') !== undefined ? tsLiteral(plain('pageRange')) : undefined],
830
- ['versionTag', plain('versionTag') !== undefined ? tsLiteral(plain('versionTag')) : undefined],
831
- ['description', plain('description') ?? nested('description') !== undefined
832
- ? tsLiteral((plain('description') ?? nested('description'))) : undefined],
833
- ])})`;
834
- }
835
804
  // Every family this SDK AUTHORS is recovered above — a guard test asserts
836
805
  // it (`flow-decompile-families.test.ts`). Reaching here means a node type
837
806
  // the SDK cannot author: a hand-written file, a newer platform family, or