@uipath/maestro-builder-sdk 6.46.1 → 6.46.3

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.
@@ -432,7 +432,13 @@ export interface TaskRef {
432
432
  * Options for {@link TaskBuilder.businessRule}.
433
433
  */
434
434
  export interface BusinessRuleOptions {
435
- /** The Orchestrator folder the rule is deployed in, e.g. `Shared`. */
435
+ /**
436
+ * The Orchestrator folder the rule is deployed in, e.g. `Shared`. For a rule
437
+ * built in the same solution as the case, pass `'solution_folder'` (the folder
438
+ * `registry search --local` reports): the binding is keyed
439
+ * `solution_folder.<name>` and its folderPath is left empty, so the case runs
440
+ * the rule from its own deployed folder.
441
+ */
436
442
  folder: string;
437
443
  /**
438
444
  * The rule's whole result schema, as `uip maestro case tasks describe --type
@@ -759,7 +765,7 @@ declare class TaskBuilder {
759
765
  * Reference a published Maestro process.
760
766
  *
761
767
  * @param name - The published process's name.
762
- * @param opts - `folder` — the Orchestrator folder it lives in.
768
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
763
769
  * @returns This builder, so calls chain.
764
770
  */
765
771
  process(name: string, opts?: {
@@ -782,7 +788,7 @@ declare class TaskBuilder {
782
788
  * Reference a published agent.
783
789
  *
784
790
  * @param name - The published agent's name.
785
- * @param opts - `folder` — the Orchestrator folder it lives in.
791
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
786
792
  * @returns This builder, so calls chain.
787
793
  */
788
794
  agent(name: string, opts?: {
@@ -792,7 +798,7 @@ declare class TaskBuilder {
792
798
  * Reference a published RPA process.
793
799
  *
794
800
  * @param name - The published RPA process's name.
795
- * @param opts - `folder` — the Orchestrator folder it lives in.
801
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
796
802
  * @returns This builder, so calls chain.
797
803
  */
798
804
  rpa(name: string, opts?: {
@@ -802,7 +808,7 @@ declare class TaskBuilder {
802
808
  * Reference a published API workflow.
803
809
  *
804
810
  * @param name - The published API workflow's name.
805
- * @param opts - `folder` — the Orchestrator folder it lives in.
811
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
806
812
  * @returns This builder, so calls chain.
807
813
  */
808
814
  apiWorkflow(name: string, opts?: {
@@ -814,7 +820,7 @@ declare class TaskBuilder {
814
820
  * io-binding as reference-mode tasks.
815
821
  *
816
822
  * @param name - The published child case's name.
817
- * @param opts - `folder` — the Orchestrator folder it lives in.
823
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
818
824
  * @returns This builder, so calls chain.
819
825
  */
820
826
  caseManagement(name: string, opts?: {
@@ -824,7 +830,7 @@ declare class TaskBuilder {
824
830
  * Reference a published Maestro Flow.
825
831
  *
826
832
  * @param name - The published Flow's name.
827
- * @param opts - `folder` — the Orchestrator folder it lives in.
833
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
828
834
  * @returns This builder, so calls chain.
829
835
  */
830
836
  flowProcess(name: string, opts?: {
@@ -190,7 +190,7 @@ class TaskBuilder {
190
190
  * Reference a published Maestro process.
191
191
  *
192
192
  * @param name - The published process's name.
193
- * @param opts - `folder` — the Orchestrator folder it lives in.
193
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
194
194
  * @returns This builder, so calls chain.
195
195
  */
196
196
  process(name, opts) {
@@ -216,7 +216,7 @@ class TaskBuilder {
216
216
  * Reference a published agent.
217
217
  *
218
218
  * @param name - The published agent's name.
219
- * @param opts - `folder` — the Orchestrator folder it lives in.
219
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
220
220
  * @returns This builder, so calls chain.
221
221
  */
222
222
  agent(name, opts) {
@@ -226,7 +226,7 @@ class TaskBuilder {
226
226
  * Reference a published RPA process.
227
227
  *
228
228
  * @param name - The published RPA process's name.
229
- * @param opts - `folder` — the Orchestrator folder it lives in.
229
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
230
230
  * @returns This builder, so calls chain.
231
231
  */
232
232
  rpa(name, opts) {
@@ -236,7 +236,7 @@ class TaskBuilder {
236
236
  * Reference a published API workflow.
237
237
  *
238
238
  * @param name - The published API workflow's name.
239
- * @param opts - `folder` — the Orchestrator folder it lives in.
239
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
240
240
  * @returns This builder, so calls chain.
241
241
  */
242
242
  apiWorkflow(name, opts) {
@@ -248,7 +248,7 @@ class TaskBuilder {
248
248
  * io-binding as reference-mode tasks.
249
249
  *
250
250
  * @param name - The published child case's name.
251
- * @param opts - `folder` — the Orchestrator folder it lives in.
251
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
252
252
  * @returns This builder, so calls chain.
253
253
  */
254
254
  caseManagement(name, opts) {
@@ -258,7 +258,7 @@ class TaskBuilder {
258
258
  * Reference a published Maestro Flow.
259
259
  *
260
260
  * @param name - The published Flow's name.
261
- * @param opts - `folder` — the Orchestrator folder it lives in.
261
+ * @param opts - `folder` — the Orchestrator folder it lives in, or `'solution_folder'` (the default when omitted) for one built in this solution. Compile binds the task through a name/folderPath pair of root bindings keyed `<folder>.<name>`.
262
262
  * @returns This builder, so calls chain.
263
263
  */
264
264
  flowProcess(name, opts) {
@@ -483,6 +483,17 @@ function checkStage(stage, diags, refStage, refPlacement) {
483
483
  message: `Task "${t.name}" binding "${'var' in invalidBinding ? invalidBinding.var : invalidBinding.name}" has unsupported type ${JSON.stringify(invalidBinding.type)}. Use a type name such as "string" or "jsonSchema"; jsonSchema(...) is only for declaring a structured case variable.`,
484
484
  where: t.name,
485
485
  });
486
+ // The builder binds a resource task itself: it mints the name/folderPath
487
+ // binding pair from the literal name and folder. A `=bindings.<id>` string
488
+ // passed as either becomes part of the binding key, which then names no
489
+ // resource (`=bindings.f.=bindings.n`) and dangles at deploy.
490
+ if (t.ref && [t.ref.name, t.ref.folderPath].some((v) => v.trim().startsWith('=bindings.')))
491
+ diags.push({
492
+ level: 'error',
493
+ code: 'RESOURCE_REF_IS_BINDING',
494
+ message: `Task "${t.name}" passes a "=bindings.<id>" reference as its resource name or folder. Pass the resource's own name and its folder, e.g. .agent('Contract Analysis', { folder: 'Shared/Legal' }); compile writes the name/folderPath binding pair keyed '<folder>.<name>' and points the task at it.`,
495
+ where: t.name,
496
+ });
486
497
  if (t.kind === 'business-rule') {
487
498
  if (!t.ref || !t.ref.name.trim() || !t.ref.folderPath.trim())
488
499
  diags.push({
@@ -1,3 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { type CliExitCode } from '../cli-run.js';
3
3
  export declare function run(argv: string[]): Promise<CliExitCode>;
4
+ /**
5
+ * Write the entry point's id onto its trigger, as `data.inputs.entryPointId`.
6
+ *
7
+ * The Case Designer pairs a trigger with its entry point through this field.
8
+ * A trigger without it gets a fresh GUID on the first open, and the entry
9
+ * point's `uniqueId` is rewritten to match, so the entry point changes identity
10
+ * (MST-16422; `validate --strict` warns STRICT_TRIGGER_ENTRY_POINT_ID). The
11
+ * Designer writes it last among the inputs, so it is appended.
12
+ */
13
+ export declare function stampEntryPointId(trigger: any, uniqueId: string): void;
@@ -28,6 +28,7 @@ import { defaultBindingsFile } from '../workdir.js';
28
28
  import { flagValue, runWhenInvokedDirectly } from '../cli-run.js';
29
29
  import { ensureTypeScriptRuntime } from '../node-runtime.js';
30
30
  import { CASE_FORMAT_PROFILE } from './format-profile.js';
31
+ import { isInSolutionKey, SOLUTION_FOLDER } from './resource-identity.js';
31
32
  import { sha256Uuid } from '../core/stable-id.js';
32
33
  import { isRecord } from '../core/node-envelope.js';
33
34
  export async function run(argv) {
@@ -81,9 +82,11 @@ export async function run(argv) {
81
82
  reportDiagnostics([...diags, thrownDiagnostic(error)]);
82
83
  throw error;
83
84
  }
85
+ // Entry points first: each trigger carries its entry point's `uniqueId` as
86
+ // `data.inputs.entryPointId`, so the plan is written after the ids are known.
87
+ const triggers = syncEntryPoints(caseplan, out);
84
88
  writeJsonFile(out, caseplan);
85
89
  const stages = caseplan.nodes.filter((n) => String(n.type).endsWith('Stage')).length;
86
- const triggers = syncEntryPoints(caseplan, out);
87
90
  const epNote = triggers >= 0 ? `, synced entry-points.json (${triggers} trigger(s))` : '';
88
91
  const resources = syncBindingsV2(caseplan, out);
89
92
  const bindingsNote = resources >= 0 ? `, synced bindings_v2.json (${resources} resource(s))` : '';
@@ -229,6 +232,25 @@ function describedCaseFileName(dir) {
229
232
  function packagedCaseFileName(dir, outPath) {
230
233
  return shippedCasePlanName(dir) ?? basename(outPath);
231
234
  }
235
+ /**
236
+ * Write the entry point's id onto its trigger, as `data.inputs.entryPointId`.
237
+ *
238
+ * The Case Designer pairs a trigger with its entry point through this field.
239
+ * A trigger without it gets a fresh GUID on the first open, and the entry
240
+ * point's `uniqueId` is rewritten to match, so the entry point changes identity
241
+ * (MST-16422; `validate --strict` warns STRICT_TRIGGER_ENTRY_POINT_ID). The
242
+ * Designer writes it last among the inputs, so it is appended.
243
+ */
244
+ export function stampEntryPointId(trigger, uniqueId) {
245
+ if (typeof uniqueId !== 'string' || !uniqueId)
246
+ return;
247
+ trigger.data ??= {};
248
+ const inputs = trigger.data.inputs;
249
+ if (inputs !== undefined && (typeof inputs !== 'object' || inputs === null || Array.isArray(inputs)))
250
+ return;
251
+ const { entryPointId: _old, ...rest } = (inputs ?? {});
252
+ trigger.data.inputs = { ...rest, entryPointId: uniqueId };
253
+ }
232
254
  /**
233
255
  * Keep the sibling `entry-points.json` in step with the caseplan's trigger nodes:
234
256
  * the runtime discovers triggers there, so a trigger node with no matching
@@ -251,9 +273,11 @@ function syncEntryPoints(caseplan, outPath) {
251
273
  // Project the case's In/Out args into this trigger's input/output schemas
252
274
  // (In-args scoped to this trigger by elementId; Out-args to every entry).
253
275
  const { input, output } = entryPointIO(caseplan.variables, n.id);
276
+ const uniqueId = prev?.uniqueId ?? sha256Uuid(`entrypoint:${n.id}`);
277
+ stampEntryPointId(n, uniqueId);
254
278
  return {
255
279
  filePath: `/content/${base}.bpmn#${n.id}`,
256
- uniqueId: prev?.uniqueId ?? sha256Uuid(`entrypoint:${n.id}`),
280
+ uniqueId,
257
281
  type: 'CaseManagement',
258
282
  input,
259
283
  output,
@@ -268,7 +292,8 @@ function syncEntryPoints(caseplan, outPath) {
268
292
  * artifact. `resources refresh` reads bindings_v2.json, not caseplan.json, and
269
293
  * succeeds with zero warnings when the sidecar is stale or empty.
270
294
  *
271
- * Non-connector resources come from root binding pairs and referenced tasks.
295
+ * Non-connector resources come from the root binding rows only, as in
296
+ * `uip maestro case bindings sync`.
272
297
  * A connector's connection and folder relationship comes from the context[]
273
298
  * that references both binding ids.
274
299
  * No-op when compiling outside a scaffolded project.
@@ -284,59 +309,29 @@ function syncBindingsV2(caseplan, outPath) {
284
309
  .filter((binding) => typeof binding.id === 'string')
285
310
  .map((binding) => [binding.id, binding]));
286
311
  const resources = [];
287
- const grouped = new Map();
312
+ // One entry per (resource, resourceKey) group of root binding rows, in the
313
+ // shape `uip maestro case bindings sync` derives (`deriveCaseBindingsV2` in
314
+ // cli case-bindings-service.ts). `validate --strict` compares the two
315
+ // entry by entry as serialized JSON, so field order matters as well as
316
+ // field values.
317
+ const groups = new Map();
288
318
  for (const binding of bindings) {
289
319
  const resource = stringValue(binding.resource);
290
320
  const key = stringValue(binding.resourceKey);
291
- if (!resource || !key || resource.toLowerCase() === 'connection')
321
+ if (!resource || !key || typeof binding.propertyAttribute !== 'string')
322
+ continue;
323
+ if (resource.toLowerCase() === 'connection')
292
324
  continue;
293
325
  const groupKey = `${resource}\0${key}`;
294
- let group = grouped.get(groupKey);
295
- if (!group) {
296
- group = { resource, key, value: {} };
297
- grouped.set(groupKey, group);
298
- }
299
- const property = stringValue(binding.propertyAttribute) ?? stringValue(binding.name);
300
- if (property)
301
- group.value[property] = { defaultValue: binding.default ?? key };
302
- const subType = stringValue(binding.resourceSubType);
303
- if (subType)
304
- group.subType = subType;
326
+ const group = groups.get(groupKey);
327
+ if (group)
328
+ group.push(binding);
329
+ else
330
+ groups.set(groupKey, [binding]);
305
331
  }
306
- const referenceTaskSubTypes = new Map([
307
- ['process', 'ProcessOrchestration'],
308
- ['agent', 'Agent'],
309
- ['rpa', undefined],
310
- ['api-workflow', 'Api'],
311
- ['case-management', 'CaseManagement'],
312
- ['flow-process', 'ProcessOrchestration'],
313
- ]);
314
332
  const seenConnections = new Set();
315
333
  const connectionResources = [];
316
334
  visitRecords(caseplan, (record) => {
317
- const type = stringValue(record.type);
318
- const data = isRecord(record.data) ? record.data : undefined;
319
- if (type && data && referenceTaskSubTypes.has(type)) {
320
- const name = referencedBindingValue(data.name, byId);
321
- const folderPath = referencedBindingValue(data.folderPath, byId);
322
- if (name && folderPath) {
323
- const key = referencedBindingResourceKey(data.name, byId)
324
- ?? referencedBindingResourceKey(data.folderPath, byId)
325
- ?? `${folderPath}.${name}`;
326
- const groupKey = `process\0${key}`;
327
- if (!grouped.has(groupKey)) {
328
- grouped.set(groupKey, {
329
- resource: 'process',
330
- key,
331
- value: {
332
- name: { defaultValue: name },
333
- folderPath: { defaultValue: folderPath },
334
- },
335
- subType: referenceTaskSubTypes.get(type),
336
- });
337
- }
338
- }
339
- }
340
335
  if (!Array.isArray(record.context))
341
336
  return;
342
337
  const context = new Map(record.context
@@ -359,23 +354,126 @@ function syncBindingsV2(caseplan, outPath) {
359
354
  metadata: { connector },
360
355
  });
361
356
  });
362
- for (const group of grouped.values()) {
363
- resources.push({
364
- resource: group.resource,
365
- key: group.key,
366
- value: group.value,
367
- // A BusinessRule entry carries the bindings version deploy needs to
368
- // override its folder — the metadata `uip maestro case bindings sync`
369
- // writes for it (no subType).
370
- metadata: group.subType
371
- ? { subType: group.subType }
372
- : group.resource === 'BusinessRule' ? { BindingsVersion: '2.2' } : {},
373
- });
374
- }
357
+ const labels = bindingLabels(caseplan.nodes);
358
+ for (const group of groups.values())
359
+ resources.push(sidecarResourceEntry(group, labels));
375
360
  resources.push(...connectionResources);
376
361
  writeJsonFile(sidecarPath, { version: '2.0', resources });
377
362
  return resources.length;
378
363
  }
364
+ /**
365
+ * The Case Designer's label for each value slot of an app or process entry.
366
+ * These resources get the Designer shape (cli#5003, MST-16424): an `id`, each
367
+ * slot flagged and labelled, and the `BindingsVersion` / `SolutionsSupport`
368
+ * metadata without which deploy cannot override the entry.
369
+ */
370
+ const DESIGNER_SLOT_NAMES = new Map([
371
+ ['app', { name: 'App name', folderPath: 'Folder path', solutionsSupport: true }],
372
+ ['process', { name: 'Process name', folderPath: 'Folder path', solutionsSupport: true }],
373
+ ['ixpDeployment', { name: 'name', folderPath: 'Folder path', solutionsSupport: false }],
374
+ ]);
375
+ /** The bindings version deploy needs to override an entry. */
376
+ const RESOURCE_BINDINGS_VERSION = '2.2';
377
+ /**
378
+ * One sidecar entry for a group of root binding rows that share a resource and
379
+ * a resource key. The subtype comes from the rows only: a Flow's rows carry
380
+ * none, so its entry has none either, as the Designer saves it.
381
+ *
382
+ * `metadata.ActivityName` labels a Designer-shaped entry with the first task
383
+ * that references one of its rows ({@link bindingLabels}).
384
+ */
385
+ function sidecarResourceEntry(group, labels) {
386
+ const resource = group[0].resource;
387
+ const key = group[0].resourceKey;
388
+ const attributeDefault = (attribute) => {
389
+ const found = group.find((binding) => binding.propertyAttribute === attribute)?.default;
390
+ return typeof found === 'string' ? found : undefined;
391
+ };
392
+ // An in-solution resource binds an empty folderPath in the caseplan, while
393
+ // the sidecar names its folder by the sentinel the key carries.
394
+ const folderPath = isInSolutionKey(key) ? SOLUTION_FOLDER : attributeDefault('folderPath');
395
+ const name = attributeDefault('name');
396
+ const subType = group.map((binding) => binding.resourceSubType).find((found) => found !== undefined);
397
+ const designer = DESIGNER_SLOT_NAMES.get(resource);
398
+ if (designer) {
399
+ const value = {};
400
+ if (folderPath !== undefined) {
401
+ value.folderPath = { defaultValue: folderPath, isExpression: false, displayName: designer.folderPath };
402
+ }
403
+ if (name !== undefined) {
404
+ value.name = { defaultValue: name, isExpression: false, displayName: designer.name };
405
+ }
406
+ const activityName = group
407
+ .map((binding) => (typeof binding.id === 'string' ? labels.get(binding.id) : undefined))
408
+ .find((found) => found !== undefined);
409
+ return {
410
+ resource,
411
+ key,
412
+ id: `${resource}${key}`,
413
+ value,
414
+ metadata: {
415
+ ...(activityName !== undefined && { ActivityName: activityName }),
416
+ BindingsVersion: RESOURCE_BINDINGS_VERSION,
417
+ DisplayLabel: 'name',
418
+ ...(designer.solutionsSupport && { SolutionsSupport: 'true' }),
419
+ ...(subType !== undefined && { subType }),
420
+ },
421
+ };
422
+ }
423
+ const value = {};
424
+ if (name !== undefined)
425
+ value.name = { defaultValue: name };
426
+ if (folderPath !== undefined)
427
+ value.folderPath = { defaultValue: folderPath };
428
+ // A solution-resource picker (such as a Data Fabric entity) carries its folder key as a slot of its own.
429
+ const folderKey = attributeDefault('folderKey');
430
+ if (folderKey !== undefined)
431
+ value.folderKey = { defaultValue: folderKey };
432
+ return {
433
+ resource,
434
+ key,
435
+ value,
436
+ metadata: {
437
+ BindingsVersion: RESOURCE_BINDINGS_VERSION,
438
+ ...(subType !== undefined && { subType }),
439
+ },
440
+ };
441
+ }
442
+ /**
443
+ * The label of the first object that references each binding id
444
+ * (`=bindings.<id>`), in document order: the nearest enclosing `displayName`,
445
+ * `display.label` or `label`. The same walk as the CLI's
446
+ * `collectBindingLabels`, so both name an entry after the same task.
447
+ */
448
+ function bindingLabels(nodes) {
449
+ const found = new Map();
450
+ const ownLabel = (node) => {
451
+ const display = isRecord(node.display) ? node.display.label : undefined;
452
+ return [node.displayName, display, node.label].map(stringValue).find((label) => label !== undefined);
453
+ };
454
+ const walk = (node, label) => {
455
+ if (typeof node === 'string') {
456
+ if (label !== undefined && node.startsWith('=bindings.')) {
457
+ const id = node.slice('=bindings.'.length);
458
+ if (!found.has(id))
459
+ found.set(id, label);
460
+ }
461
+ return;
462
+ }
463
+ if (Array.isArray(node)) {
464
+ for (const item of node)
465
+ walk(item, label);
466
+ return;
467
+ }
468
+ if (!isRecord(node))
469
+ return;
470
+ const scoped = ownLabel(node) ?? label;
471
+ for (const value of Object.values(node))
472
+ walk(value, scoped);
473
+ };
474
+ walk(nodes ?? [], undefined);
475
+ return found;
476
+ }
379
477
  function referencedBindingValue(value, bindings) {
380
478
  const ref = stringValue(value);
381
479
  if (!ref)
@@ -385,12 +483,6 @@ function referencedBindingValue(value, bindings) {
385
483
  const binding = bindings.get(ref.slice('=bindings.'.length));
386
484
  return binding && (stringValue(binding.default) ?? stringValue(binding.resourceKey));
387
485
  }
388
- function referencedBindingResourceKey(value, bindings) {
389
- const ref = stringValue(value);
390
- if (!ref?.startsWith('=bindings.'))
391
- return undefined;
392
- return stringValue(bindings.get(ref.slice('=bindings.'.length))?.resourceKey);
393
- }
394
486
  function visitRecords(value, visit) {
395
487
  if (Array.isArray(value)) {
396
488
  for (const item of value)
@@ -4,6 +4,7 @@ import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
4
4
  import { preserveSchemaVersion } from '../schema-version.js';
5
5
  import { ownEntry } from '../core/own-entry.js';
6
6
  import { isUnrestorableSlotId } from './preserve.js';
7
+ import { isInSolutionKey, SOLUTION_FOLDER } from './resource-identity.js';
7
8
  const TASK_KIND_METHOD = {
8
9
  process: 'process',
9
10
  agent: 'agent',
@@ -133,6 +134,10 @@ function encodedSubscriptionConfig(context, owner) {
133
134
  throw new Error(`${owner} has malformed subscription metadata configuration JSON.`);
134
135
  }
135
136
  }
137
+ /** A plain object with no keys — the Designer's spelling of an empty subscription body. */
138
+ function isEmptyObject(value) {
139
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
140
+ }
136
141
  function subscriptionWhere(bag, owner) {
137
142
  const inputs = bag.inputs;
138
143
  if (!Array.isArray(inputs) || inputs.length !== 1)
@@ -142,6 +147,11 @@ function subscriptionWhere(bag, owner) {
142
147
  throw new Error(`${owner} has an unsupported subscription input.`);
143
148
  if (row.value === '' && row.body === undefined)
144
149
  return undefined;
150
+ // The Case Designer saves a subscription with no parameters (an HTTP webhook,
151
+ // say) as `body: {}` with no `value`. That is the same "no where" the SDK
152
+ // writes as `value: ''`, not an unsupported body.
153
+ if (isEmptyObject(row.body) && (row.value === undefined || row.value === ''))
154
+ return undefined;
145
155
  const queryParams = row.body?.queryParams;
146
156
  if (!queryParams || typeof queryParams !== 'object' || Array.isArray(queryParams))
147
157
  throw new Error(`${owner} has an unsupported subscription body.`);
@@ -334,11 +344,39 @@ function bindingName(value, refs) {
334
344
  const id = value.slice('=bindings.'.length);
335
345
  return refs.bindingById.get(id)?.id ?? id;
336
346
  }
337
- function bindingDefault(value, refs) {
347
+ /**
348
+ * The literal a resource task's `data.name` / `data.folderPath` names: the
349
+ * value itself, or the `default` of the root binding row a `=bindings.<id>`
350
+ * reference points at. A reference the plan's root `bindings[]` cannot answer
351
+ * (no such row, or a row with no `default`) is refused: the plan names no
352
+ * resource there, and emitting the reference as the name would only fail
353
+ * again at compile.
354
+ */
355
+ function bindingDefault(value, refs, owner, field) {
338
356
  if (typeof value !== 'string' || !value.startsWith('=bindings.'))
339
357
  return value;
340
358
  const id = value.slice('=bindings.'.length);
341
- return refs.bindingById.get(id)?.default ?? value;
359
+ const row = refs.bindingById.get(id);
360
+ if (typeof row?.default !== 'string')
361
+ throw new Error(`${owner} data.${field} is "${value}", but the plan's root bindings ${row ? `row "${id}" has no default` : `declare no row "${id}"`}, `
362
+ + `so decompile cannot recover the resource ${field === 'name' ? 'name' : 'folder'}. `
363
+ + `Give the task a name/folderPath binding pair whose rows carry the resource's name and folder as their defaults (what Studio Web writes), then decompile again.`);
364
+ return row.default;
365
+ }
366
+ /**
367
+ * The folder a resource task's `folderPath` reference names, as the author
368
+ * writes it. An in-solution resource binds an empty folderPath under a
369
+ * `solution_folder.<name>` key; the builder spells that folder `solution_folder`
370
+ * (resource-identity.ts), so lift the sentinel back rather than the empty
371
+ * default, which would recompile to the key `.<name>`.
372
+ */
373
+ function bindingFolder(value, refs, owner) {
374
+ if (typeof value === 'string' && value.startsWith('=bindings.')) {
375
+ const row = refs.bindingById.get(value.slice('=bindings.'.length));
376
+ if (typeof row?.resourceKey === 'string' && isInSolutionKey(row.resourceKey))
377
+ return SOLUTION_FOLDER;
378
+ }
379
+ return bindingDefault(value, refs, owner, 'folderPath');
342
380
  }
343
381
  function selectNextStageByExit(plan, refs) {
344
382
  const raw = plan.metadata?.waitForHumanSelectNextStageDFConnector;
@@ -505,10 +543,17 @@ function externalCall(task, refs, library) {
505
543
  throw new Error(`${owner} has unsupported serviceType "${String(data.serviceType ?? '')}".`);
506
544
  if (!Array.isArray(data.context))
507
545
  throw new Error(`${owner} requires a resolved context array.`);
546
+ // A trailing `activityConfigurationVersion: v2` row only picks the Designer
547
+ // panel; compile writes it back from the plan's `intsvcActivityConfig` flag.
548
+ const rows = data.context;
549
+ const last = rows[rows.length - 1];
550
+ const configRows = last?.name === 'activityConfigurationVersion' && last.value === 'v2' && last.type === 'string'
551
+ ? rows.slice(0, -1)
552
+ : rows;
508
553
  const expectedContext = ['activity', 'connectorKey', 'connection', 'folderKey', 'method', 'path'];
509
- if (data.context.map((row) => row.name).join('\0') !== expectedContext.join('\0'))
510
- throw new Error(`${owner} context must contain exactly ${expectedContext.join(', ')} in canonical order.`);
511
- const context = contextMap(data);
554
+ if (configRows.map((row) => row.name).join('\0') !== expectedContext.join('\0'))
555
+ throw new Error(`${owner} context must contain exactly ${expectedContext.join(', ')} in canonical order, optionally followed by activityConfigurationVersion v2.`);
556
+ const context = contextMap({ context: configRows });
512
557
  const stringValue = (name) => {
513
558
  const row = context.get(name);
514
559
  if (row?.type !== 'string' || typeof row.value !== 'string' || !row.value)
@@ -574,9 +619,12 @@ function emitTaskKind(task, refs, library) {
574
619
  throw new Error(`unresolved ${kind} task "${task.displayName ?? task.id ?? '?'}" cannot carry input or output bindings.`);
575
620
  return `.unresolved(${tsLiteral(kind)})`;
576
621
  }
577
- const name = bindingDefault(data.name, refs);
578
- const folderPath = bindingDefault(data.folderPath, refs);
579
- const folder = folderPath ? `, ${opts([['folder', tsLiteral(folderPath)]])}` : '';
622
+ const owner = `${kind} task "${task.displayName ?? task.id ?? '?'}"`;
623
+ const name = bindingDefault(data.name, refs, owner, 'name');
624
+ const folderPath = bindingFolder(data.folderPath, refs, owner);
625
+ // No folder already means the case's own solution folder
626
+ // (resource-identity.ts), so an in-solution resource decompiles to none.
627
+ const folder = folderPath && folderPath !== SOLUTION_FOLDER ? `, ${opts([['folder', tsLiteral(folderPath)]])}` : '';
580
628
  return `.${method}(${tsLiteral(name ?? '')}${folder})`;
581
629
  }
582
630
  if (isAuthorableBusinessRule(task))
@@ -642,8 +690,8 @@ function isBusinessRuleOwnOutput(row) {
642
690
  function businessRuleCall(task, refs) {
643
691
  const data = task.data ?? {};
644
692
  const owner = `business-rule task "${task.displayName ?? task.id ?? '?'}"`;
645
- const name = bindingDefault(data.name, refs);
646
- const folderPath = bindingDefault(data.folderPath, refs);
693
+ const name = bindingDefault(data.name, refs, owner, 'name');
694
+ const folderPath = bindingFolder(data.folderPath, refs, owner);
647
695
  if (typeof name !== 'string' || !name || typeof folderPath !== 'string')
648
696
  throw new Error(`${owner} must name its rule and folder (data.name / data.folderPath).`);
649
697
  const outputRow = (Array.isArray(data.outputs) ? data.outputs : [])
@@ -0,0 +1,44 @@
1
+ /**
2
+ * How a Case resource task names the resource it runs: the `resourceKey` its
3
+ * root binding pair shares, and the `folderPath` default the runtime reads.
4
+ *
5
+ * For a resource deployed to a tenant folder the two agree:
6
+ *
7
+ * folder 'Shared/Finance', name 'CreditRisk'
8
+ * -> resourceKey 'Shared/Finance.CreditRisk', folderPath 'Shared/Finance'
9
+ *
10
+ * For a sibling built inside the same solution they do not. Its folder is the
11
+ * `solution_folder` sentinel (what `uip maestro case registry search --local`
12
+ * and `resources/solution_folder/...` report), and the platform splits it:
13
+ *
14
+ * folder 'solution_folder', name 'CreditRisk'
15
+ * -> resourceKey 'solution_folder.CreditRisk', folderPath ''
16
+ *
17
+ * The key keeps the sentinel because deploy and `resources refresh` match the
18
+ * resource by it. The `folderPath` default is empty because the running case
19
+ * starts the sibling in its own deployed folder; a literal `solution_folder`
20
+ * there passes `validate` and fails the task at runtime with "Folder does not
21
+ * exist". The CLI encodes the same split: `case-bindings-service.ts` writes the
22
+ * sentinel into `bindings_v2.json` for a `solution_folder.` key, and
23
+ * `case-validate-service.ts` exempts that prefix from its key/folder agreement
24
+ * check.
25
+ */
26
+ export declare const SOLUTION_FOLDER = "solution_folder";
27
+ export interface ResourceIdentity {
28
+ /** The `resourceKey` both rows of the name/folderPath binding pair carry. */
29
+ resourceKey: string;
30
+ /** The `default` of the `folderPath` binding row. */
31
+ folderPath: string;
32
+ }
33
+ /**
34
+ * The binding identity of resource `name` authored in `folder`. No folder
35
+ * means the case's own solution folder, the same as `solution_folder`:
36
+ *
37
+ * .process('notify') -> resourceKey 'solution_folder.notify', folderPath ''
38
+ *
39
+ * (A literal `.notify` key would name no folder, and deploy and `resources
40
+ * refresh` could not match it.)
41
+ */
42
+ export declare function resourceIdentity(name: string, folder: string): ResourceIdentity;
43
+ /** True for the key of a resource built inside the same solution. */
44
+ export declare function isInSolutionKey(resourceKey: string): boolean;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * How a Case resource task names the resource it runs: the `resourceKey` its
3
+ * root binding pair shares, and the `folderPath` default the runtime reads.
4
+ *
5
+ * For a resource deployed to a tenant folder the two agree:
6
+ *
7
+ * folder 'Shared/Finance', name 'CreditRisk'
8
+ * -> resourceKey 'Shared/Finance.CreditRisk', folderPath 'Shared/Finance'
9
+ *
10
+ * For a sibling built inside the same solution they do not. Its folder is the
11
+ * `solution_folder` sentinel (what `uip maestro case registry search --local`
12
+ * and `resources/solution_folder/...` report), and the platform splits it:
13
+ *
14
+ * folder 'solution_folder', name 'CreditRisk'
15
+ * -> resourceKey 'solution_folder.CreditRisk', folderPath ''
16
+ *
17
+ * The key keeps the sentinel because deploy and `resources refresh` match the
18
+ * resource by it. The `folderPath` default is empty because the running case
19
+ * starts the sibling in its own deployed folder; a literal `solution_folder`
20
+ * there passes `validate` and fails the task at runtime with "Folder does not
21
+ * exist". The CLI encodes the same split: `case-bindings-service.ts` writes the
22
+ * sentinel into `bindings_v2.json` for a `solution_folder.` key, and
23
+ * `case-validate-service.ts` exempts that prefix from its key/folder agreement
24
+ * check.
25
+ */
26
+ export const SOLUTION_FOLDER = 'solution_folder';
27
+ const SOLUTION_FOLDER_KEY_PREFIX = `${SOLUTION_FOLDER}.`;
28
+ /**
29
+ * The binding identity of resource `name` authored in `folder`. No folder
30
+ * means the case's own solution folder, the same as `solution_folder`:
31
+ *
32
+ * .process('notify') -> resourceKey 'solution_folder.notify', folderPath ''
33
+ *
34
+ * (A literal `.notify` key would name no folder, and deploy and `resources
35
+ * refresh` could not match it.)
36
+ */
37
+ export function resourceIdentity(name, folder) {
38
+ if (folder === SOLUTION_FOLDER || !folder.trim())
39
+ return { resourceKey: `${SOLUTION_FOLDER_KEY_PREFIX}${name}`, folderPath: '' };
40
+ return { resourceKey: `${folder}.${name}`, folderPath: folder };
41
+ }
42
+ /** True for the key of a resource built inside the same solution. */
43
+ export function isInSolutionKey(resourceKey) {
44
+ return resourceKey.startsWith(SOLUTION_FOLDER_KEY_PREFIX);
45
+ }