@uipath/maestro-builder-sdk 6.46.2 → 6.46.4

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.
@@ -491,6 +491,16 @@ export interface ActionField {
491
491
  label: string;
492
492
  }>;
493
493
  }
494
+ /** The Action App an action task opens. */
495
+ export interface ActionAppRef {
496
+ /** The app's deployment title, e.g. `SimpleApprovalApp`. */
497
+ name: string;
498
+ /**
499
+ * The app's deployment folder, e.g. `Shared/Approvals`, or `'solution_folder'`
500
+ * for an app built in this solution.
501
+ */
502
+ folder: string;
503
+ }
494
504
  export interface ActionSpecData {
495
505
  title?: string;
496
506
  priority?: 'Low' | 'Medium' | 'High' | 'Critical';
@@ -500,7 +510,11 @@ export interface ActionSpecData {
500
510
  };
501
511
  /** Action Center labels (persisted as a single `data.labels` string). */
502
512
  labels?: string;
503
- /** The Action Center action-app / catalog this task instantiates. */
513
+ /**
514
+ * An Action Center task catalog, resolved by name in the app's deployment
515
+ * folder. Not the app: a catalog only groups tasks. Omit it unless the catalog
516
+ * is known to exist there.
517
+ */
504
518
  actionCatalogName?: string;
505
519
  /** Read-only context fields the assignee sees (`data.inputs[]`). */
506
520
  inputs?: ActionField[];
@@ -544,6 +558,7 @@ export interface BuiltTask {
544
558
  /** Task key + display name (also the handle used by `selected-tasks-completed`). */
545
559
  name: string;
546
560
  kind: TaskKind;
561
+ /** The published resource the task binds: a reference task's process, agent, … or an action task's Action App. */
547
562
  ref?: TaskRef;
548
563
  /** business-rule task: the rule's decision-keyed result schema, when known. */
549
564
  businessRule?: BusinessRuleSpecData;
@@ -765,7 +780,7 @@ declare class TaskBuilder {
765
780
  * Reference a published Maestro process.
766
781
  *
767
782
  * @param name - The published process's name.
768
- * @param opts - `folder` — the Orchestrator folder it lives in.
783
+ * @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>`.
769
784
  * @returns This builder, so calls chain.
770
785
  */
771
786
  process(name: string, opts?: {
@@ -788,7 +803,7 @@ declare class TaskBuilder {
788
803
  * Reference a published agent.
789
804
  *
790
805
  * @param name - The published agent's name.
791
- * @param opts - `folder` — the Orchestrator folder it lives in.
806
+ * @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>`.
792
807
  * @returns This builder, so calls chain.
793
808
  */
794
809
  agent(name: string, opts?: {
@@ -798,7 +813,7 @@ declare class TaskBuilder {
798
813
  * Reference a published RPA process.
799
814
  *
800
815
  * @param name - The published RPA process's name.
801
- * @param opts - `folder` — the Orchestrator folder it lives in.
816
+ * @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>`.
802
817
  * @returns This builder, so calls chain.
803
818
  */
804
819
  rpa(name: string, opts?: {
@@ -808,7 +823,7 @@ declare class TaskBuilder {
808
823
  * Reference a published API workflow.
809
824
  *
810
825
  * @param name - The published API workflow's name.
811
- * @param opts - `folder` — the Orchestrator folder it lives in.
826
+ * @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>`.
812
827
  * @returns This builder, so calls chain.
813
828
  */
814
829
  apiWorkflow(name: string, opts?: {
@@ -820,7 +835,7 @@ declare class TaskBuilder {
820
835
  * io-binding as reference-mode tasks.
821
836
  *
822
837
  * @param name - The published child case's name.
823
- * @param opts - `folder` — the Orchestrator folder it lives in.
838
+ * @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>`.
824
839
  * @returns This builder, so calls chain.
825
840
  */
826
841
  caseManagement(name: string, opts?: {
@@ -830,7 +845,7 @@ declare class TaskBuilder {
830
845
  * Reference a published Maestro Flow.
831
846
  *
832
847
  * @param name - The published Flow's name.
833
- * @param opts - `folder` — the Orchestrator folder it lives in.
848
+ * @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>`.
834
849
  * @returns This builder, so calls chain.
835
850
  */
836
851
  flowProcess(name: string, opts?: {
@@ -886,9 +901,13 @@ declare class TaskBuilder {
886
901
  * An Action Center human task. `recipient` may be an email (→ Type 2) or
887
902
  * `{ type, value }`. `inputs`/`outputs` declare the task's form fields — inputs
888
903
  * are read-only context the assignee sees, outputs are what they fill in.
889
- * `labels` and `actionCatalogName` tag the task and name its action app.
904
+ * `app` names the Action App the task opens (`{ name, folder }`); compile
905
+ * binds it through an `app` name/folderPath pair keyed `<folder>.<name>`.
906
+ * `labels` tag the task. `actionCatalogName` names a task catalog, which is
907
+ * resolved in the app's deployment folder; it is not the app, and a name that
908
+ * folder does not hold fails the task when it opens.
890
909
  *
891
- * @param spec - The human task: its `title`, `priority`, `recipient`, and the `inputs` / `outputs` its form shows and collects.
910
+ * @param spec - The human task: its `title`, `priority`, `recipient`, the Action `app`, and the `inputs` / `outputs` its form shows and collects.
892
911
  * @returns This builder, so calls chain.
893
912
  */
894
913
  action(spec?: {
@@ -899,6 +918,7 @@ declare class TaskBuilder {
899
918
  value: string;
900
919
  };
901
920
  labels?: string;
921
+ app?: ActionAppRef;
902
922
  actionCatalogName?: string;
903
923
  inputs?: ActionField[];
904
924
  outputs?: ActionField[];
@@ -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) {
@@ -332,13 +332,22 @@ class TaskBuilder {
332
332
  * An Action Center human task. `recipient` may be an email (→ Type 2) or
333
333
  * `{ type, value }`. `inputs`/`outputs` declare the task's form fields — inputs
334
334
  * are read-only context the assignee sees, outputs are what they fill in.
335
- * `labels` and `actionCatalogName` tag the task and name its action app.
335
+ * `app` names the Action App the task opens (`{ name, folder }`); compile
336
+ * binds it through an `app` name/folderPath pair keyed `<folder>.<name>`.
337
+ * `labels` tag the task. `actionCatalogName` names a task catalog, which is
338
+ * resolved in the app's deployment folder; it is not the app, and a name that
339
+ * folder does not hold fails the task when it opens.
336
340
  *
337
- * @param spec - The human task: its `title`, `priority`, `recipient`, and the `inputs` / `outputs` its form shows and collects.
341
+ * @param spec - The human task: its `title`, `priority`, `recipient`, the Action `app`, and the `inputs` / `outputs` its form shows and collects.
338
342
  * @returns This builder, so calls chain.
339
343
  */
340
344
  action(spec = {}) {
341
345
  this.setKind('action');
346
+ // The app is the task's resource, carried as its `ref` like every other
347
+ // bound resource, so the resource-ref rules in check and the binding pair
348
+ // in serialize both see it.
349
+ if (spec.app)
350
+ this._ref = { name: spec.app.name, folderPath: spec.app.folder };
342
351
  const recipient = typeof spec.recipient === 'string' ? { type: 2, value: spec.recipient } : spec.recipient;
343
352
  this._action = {
344
353
  title: spec.title,
@@ -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({
@@ -292,7 +292,8 @@ function syncEntryPoints(caseplan, outPath) {
292
292
  * artifact. `resources refresh` reads bindings_v2.json, not caseplan.json, and
293
293
  * succeeds with zero warnings when the sidecar is stale or empty.
294
294
  *
295
- * 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`.
296
297
  * A connector's connection and folder relationship comes from the context[]
297
298
  * that references both binding ids.
298
299
  * No-op when compiling outside a scaffolded project.
@@ -308,65 +309,29 @@ function syncBindingsV2(caseplan, outPath) {
308
309
  .filter((binding) => typeof binding.id === 'string')
309
310
  .map((binding) => [binding.id, binding]));
310
311
  const resources = [];
311
- 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();
312
318
  for (const binding of bindings) {
313
319
  const resource = stringValue(binding.resource);
314
320
  const key = stringValue(binding.resourceKey);
315
- if (!resource || !key || resource.toLowerCase() === 'connection')
321
+ if (!resource || !key || typeof binding.propertyAttribute !== 'string')
322
+ continue;
323
+ if (resource.toLowerCase() === 'connection')
316
324
  continue;
317
325
  const groupKey = `${resource}\0${key}`;
318
- let group = grouped.get(groupKey);
319
- if (!group) {
320
- group = { resource, key, value: {} };
321
- grouped.set(groupKey, group);
322
- }
323
- const property = stringValue(binding.propertyAttribute) ?? stringValue(binding.name);
324
- // An in-solution resource binds an empty folderPath in the caseplan, while
325
- // the sidecar names its folder by the sentinel the key carries — the split
326
- // `uip maestro case bindings sync` writes (resource-identity.ts).
327
- const defaultValue = property === 'folderPath' && isInSolutionKey(key)
328
- ? SOLUTION_FOLDER
329
- : binding.default ?? key;
330
- if (property)
331
- group.value[property] = { defaultValue };
332
- const subType = stringValue(binding.resourceSubType);
333
- if (subType)
334
- group.subType = subType;
326
+ const group = groups.get(groupKey);
327
+ if (group)
328
+ group.push(binding);
329
+ else
330
+ groups.set(groupKey, [binding]);
335
331
  }
336
- const referenceTaskSubTypes = new Map([
337
- ['process', 'ProcessOrchestration'],
338
- ['agent', 'Agent'],
339
- ['rpa', undefined],
340
- ['api-workflow', 'Api'],
341
- ['case-management', 'CaseManagement'],
342
- ['flow-process', 'ProcessOrchestration'],
343
- ]);
344
332
  const seenConnections = new Set();
345
333
  const connectionResources = [];
346
334
  visitRecords(caseplan, (record) => {
347
- const type = stringValue(record.type);
348
- const data = isRecord(record.data) ? record.data : undefined;
349
- if (type && data && referenceTaskSubTypes.has(type)) {
350
- const name = referencedBindingValue(data.name, byId);
351
- const folderPath = referencedBindingValue(data.folderPath, byId);
352
- if (name && folderPath) {
353
- const key = referencedBindingResourceKey(data.name, byId)
354
- ?? referencedBindingResourceKey(data.folderPath, byId)
355
- ?? `${folderPath}.${name}`;
356
- const groupKey = `process\0${key}`;
357
- if (!grouped.has(groupKey)) {
358
- grouped.set(groupKey, {
359
- resource: 'process',
360
- key,
361
- value: {
362
- name: { defaultValue: name },
363
- folderPath: { defaultValue: folderPath },
364
- },
365
- subType: referenceTaskSubTypes.get(type),
366
- });
367
- }
368
- }
369
- }
370
335
  if (!Array.isArray(record.context))
371
336
  return;
372
337
  const context = new Map(record.context
@@ -389,23 +354,126 @@ function syncBindingsV2(caseplan, outPath) {
389
354
  metadata: { connector },
390
355
  });
391
356
  });
392
- for (const group of grouped.values()) {
393
- resources.push({
394
- resource: group.resource,
395
- key: group.key,
396
- value: group.value,
397
- // A BusinessRule entry carries the bindings version deploy needs to
398
- // override its folder — the metadata `uip maestro case bindings sync`
399
- // writes for it (no subType).
400
- metadata: group.subType
401
- ? { subType: group.subType }
402
- : group.resource === 'BusinessRule' ? { BindingsVersion: '2.2' } : {},
403
- });
404
- }
357
+ const labels = bindingLabels(caseplan.nodes);
358
+ for (const group of groups.values())
359
+ resources.push(sidecarResourceEntry(group, labels));
405
360
  resources.push(...connectionResources);
406
361
  writeJsonFile(sidecarPath, { version: '2.0', resources });
407
362
  return resources.length;
408
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
+ }
409
477
  function referencedBindingValue(value, bindings) {
410
478
  const ref = stringValue(value);
411
479
  if (!ref)
@@ -415,12 +483,6 @@ function referencedBindingValue(value, bindings) {
415
483
  const binding = bindings.get(ref.slice('=bindings.'.length));
416
484
  return binding && (stringValue(binding.default) ?? stringValue(binding.resourceKey));
417
485
  }
418
- function referencedBindingResourceKey(value, bindings) {
419
- const ref = stringValue(value);
420
- if (!ref?.startsWith('=bindings.'))
421
- return undefined;
422
- return stringValue(bindings.get(ref.slice('=bindings.'.length))?.resourceKey);
423
- }
424
486
  function visitRecords(value, visit) {
425
487
  if (Array.isArray(value)) {
426
488
  for (const item of value)
@@ -344,11 +344,24 @@ function bindingName(value, refs) {
344
344
  const id = value.slice('=bindings.'.length);
345
345
  return refs.bindingById.get(id)?.id ?? id;
346
346
  }
347
- 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) {
348
356
  if (typeof value !== 'string' || !value.startsWith('=bindings.'))
349
357
  return value;
350
358
  const id = value.slice('=bindings.'.length);
351
- 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;
352
365
  }
353
366
  /**
354
367
  * The folder a resource task's `folderPath` reference names, as the author
@@ -357,13 +370,13 @@ function bindingDefault(value, refs) {
357
370
  * (resource-identity.ts), so lift the sentinel back rather than the empty
358
371
  * default, which would recompile to the key `.<name>`.
359
372
  */
360
- function bindingFolder(value, refs) {
373
+ function bindingFolder(value, refs, owner) {
361
374
  if (typeof value === 'string' && value.startsWith('=bindings.')) {
362
375
  const row = refs.bindingById.get(value.slice('=bindings.'.length));
363
376
  if (typeof row?.resourceKey === 'string' && isInSolutionKey(row.resourceKey))
364
377
  return SOLUTION_FOLDER;
365
378
  }
366
- return bindingDefault(value, refs);
379
+ return bindingDefault(value, refs, owner, 'folderPath');
367
380
  }
368
381
  function selectNextStageByExit(plan, refs) {
369
382
  const raw = plan.metadata?.waitForHumanSelectNextStageDFConnector;
@@ -606,9 +619,12 @@ function emitTaskKind(task, refs, library) {
606
619
  throw new Error(`unresolved ${kind} task "${task.displayName ?? task.id ?? '?'}" cannot carry input or output bindings.`);
607
620
  return `.unresolved(${tsLiteral(kind)})`;
608
621
  }
609
- const name = bindingDefault(data.name, refs);
610
- const folderPath = bindingDefault(data.folderPath, refs);
611
- 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)]])}` : '';
612
628
  return `.${method}(${tsLiteral(name ?? '')}${folder})`;
613
629
  }
614
630
  if (isAuthorableBusinessRule(task))
@@ -628,6 +644,7 @@ function emitTaskKind(task, refs, library) {
628
644
  ['priority', data.priority && data.priority !== 'Medium' ? tsLiteral(data.priority) : undefined],
629
645
  ['recipient', recipient],
630
646
  ['labels', data.labels !== undefined ? tsLiteral(data.labels) : undefined],
647
+ ['app', actionApp(data, refs, task)],
631
648
  ['actionCatalogName', data.actionCatalogName !== undefined ? tsLiteral(data.actionCatalogName) : undefined],
632
649
  ['inputs', actionFields(data.inputs)],
633
650
  ['outputs', actionFields(data.outputs)],
@@ -663,6 +680,21 @@ function emitTaskKind(task, refs, library) {
663
680
  });
664
681
  return `.__UNRESOLVED_TASK_KIND__(${tsLiteral(kind)}, ${tsLiteral(data)})`;
665
682
  }
683
+ /**
684
+ * The `app: { name, folder }` of an action task bound to its Action App, or
685
+ * undefined for one with no app. Dropping it lost the app binding on every
686
+ * round trip of a Studio Web plan (#1240).
687
+ */
688
+ function actionApp(data, refs, task) {
689
+ if (data.name === undefined && data.folderPath === undefined)
690
+ return undefined;
691
+ const owner = `action task "${task.displayName ?? task.id ?? '?'}"`;
692
+ const name = bindingDefault(data.name, refs, owner, 'name');
693
+ const folder = bindingFolder(data.folderPath, refs, owner);
694
+ if (typeof name !== 'string' || !name || typeof folder !== 'string')
695
+ throw new Error(`${owner} names its app by neither a binding nor a literal name and folder.`);
696
+ return opts([['name', tsLiteral(name)], ['folder', tsLiteral(folder)]]);
697
+ }
666
698
  /** A business-rule task `.businessRule()` can rebuild: v3 only. */
667
699
  function isAuthorableBusinessRule(task) {
668
700
  return task.type === 'business-rule' && task.data?.version === 'v3';
@@ -674,8 +706,8 @@ function isBusinessRuleOwnOutput(row) {
674
706
  function businessRuleCall(task, refs) {
675
707
  const data = task.data ?? {};
676
708
  const owner = `business-rule task "${task.displayName ?? task.id ?? '?'}"`;
677
- const name = bindingDefault(data.name, refs);
678
- const folderPath = bindingFolder(data.folderPath, refs);
709
+ const name = bindingDefault(data.name, refs, owner, 'name');
710
+ const folderPath = bindingFolder(data.folderPath, refs, owner);
679
711
  if (typeof name !== 'string' || !name || typeof folderPath !== 'string')
680
712
  throw new Error(`${owner} must name its rule and folder (data.name / data.folderPath).`);
681
713
  const outputRow = (Array.isArray(data.outputs) ? data.outputs : [])
@@ -30,7 +30,15 @@ export interface ResourceIdentity {
30
30
  /** The `default` of the `folderPath` binding row. */
31
31
  folderPath: string;
32
32
  }
33
- /** The binding identity of resource `name` authored in `folder`. */
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
+ */
34
42
  export declare function resourceIdentity(name: string, folder: string): ResourceIdentity;
35
43
  /** True for the key of a resource built inside the same solution. */
36
44
  export declare function isInSolutionKey(resourceKey: string): boolean;
@@ -25,9 +25,17 @@
25
25
  */
26
26
  export const SOLUTION_FOLDER = 'solution_folder';
27
27
  const SOLUTION_FOLDER_KEY_PREFIX = `${SOLUTION_FOLDER}.`;
28
- /** The binding identity of resource `name` authored in `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
+ */
29
37
  export function resourceIdentity(name, folder) {
30
- if (folder === SOLUTION_FOLDER)
38
+ if (folder === SOLUTION_FOLDER || !folder.trim())
31
39
  return { resourceKey: `${SOLUTION_FOLDER_KEY_PREFIX}${name}`, folderPath: '' };
32
40
  return { resourceKey: `${folder}.${name}`, folderPath: folder };
33
41
  }