@uipath/maestro-builder-sdk 6.70.0 → 6.70.2

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.
@@ -2071,7 +2071,7 @@ defaultScope = () => ownerId) {
2071
2071
  if (skip.has(v.id))
2072
2072
  continue;
2073
2073
  const type = varType(v.type);
2074
- const def = v.default === undefined ? undefined : decodeDefault(v.default, v.type);
2074
+ const def = v.default === undefined ? undefined : decodeDefault(v.default, v.type, v.id);
2075
2075
  entries.push(`{ id: ${tsText(v.id)}, type: ${tsText(type)}${inner([
2076
2076
  ['direction', direction === 'inputOutput' ? undefined : tsText(direction)],
2077
2077
  ['name', v.name !== undefined && v.name !== v.id ? tsText(v.name) : undefined],
@@ -2118,11 +2118,19 @@ function bindingIsReferenced(xml, id) {
2118
2118
  * anything non-finite, so it was right by accident; this makes the rule explicit for
2119
2119
  * every type.
2120
2120
  */
2121
- function decodeDefault(raw, wire) {
2121
+ function decodeDefault(raw, wire, id) {
2122
2122
  if (raw.startsWith('='))
2123
2123
  return tsText(raw); // an expression, not a value of `wire`
2124
- if (wire === 'json')
2125
- return raw; // already JSON — emit it verbatim
2124
+ if (wire === 'json') {
2125
+ // emitted unquoted, so it must be JSON
2126
+ try {
2127
+ JSON.parse(raw);
2128
+ }
2129
+ catch {
2130
+ throw new Error(`decompile: variable ${JSON.stringify(id)} has type "json" but its default ${JSON.stringify(raw)} is not JSON.`);
2131
+ }
2132
+ return raw;
2133
+ }
2126
2134
  if (wire === 'number')
2127
2135
  return Number.isFinite(Number(raw)) ? String(Number(raw)) : tsText(raw);
2128
2136
  // Only the two literals are booleans. Anything else — an empty `default=""`, which
@@ -20,8 +20,10 @@
20
20
  import type { TypeDesc } from '../core/expr.js';
21
21
  import { type ActionSpec, type ConnectorOpts, type EventSubscription, type TriggerOptions } from '../core/actions.js';
22
22
  import { type ConnectorDescriptor, type TriggerDescriptor } from '../core/connectors.js';
23
+ import type { CarriedConnectorField } from '../core/carried-connector-fields.js';
24
+ export type { CarriedConnectorField, CarriedFieldLocation } from '../core/carried-connector-fields.js';
23
25
  export { preserveCaseJson } from './preserve.js';
24
- export type { CaseJsonPreservation, PreservedCaseNode, PreservedTaskFields, PreservedVariableLane } from './preserve.js';
26
+ export type { CaseJsonPreservation, PreservedCaseNode, PreservedConnectorConfiguration, PreservedTaskFields, PreservedVariableLane } from './preserve.js';
25
27
  export { CaseBuildError } from './case-expr-check.js';
26
28
  export type { CaseLocatedDiagnostic } from './case-expr-check.js';
27
29
  export type CaseRuleType = 'case-entered' | 'required-tasks-completed' | 'required-stages-completed' | 'selected-stage-completed' | 'selected-stage-exited' | 'selected-tasks-completed' | 'current-stage-entered' | 'adhoc' | 'runs-sequentially' | 'user-selected-stage' | 'wait-for-connector' | 'sla-status-change' | 'api-event';
@@ -424,7 +426,34 @@ export type WaitConnectorSpec = WaitConnectorPlaceholderSpec | EventSubscription
424
426
  */
425
427
  export type ConnectorSpecData = Extract<ActionSpec, {
426
428
  kind: 'connector';
427
- }>;
429
+ }> & {
430
+ /** The plan's own declarations of fields the library lacks; see {@link CaseConnectorOpts.carriedFields}. */
431
+ carriedFields?: readonly CarriedConnectorField[];
432
+ };
433
+ /**
434
+ * {@link TaskBuilder.connector}'s options: the Flow `connector()` options, plus
435
+ * the fields a decompiled task carries.
436
+ */
437
+ export interface CaseConnectorOpts extends ConnectorOpts {
438
+ /**
439
+ * Fields the plan this task was decompiled from declares and the connector
440
+ * library does not. Written by `decompile`; leave it in place, and do not add
441
+ * to it by hand.
442
+ *
443
+ * @remarks
444
+ * Studio Web saves a connector task against the live connector, which can be
445
+ * newer than the published library. Decompile copies the saved declaration of
446
+ * each such field (from the task's `fieldsContainer.inputFields`), so `check`
447
+ * and `compile` accept the value the plan already has instead of refusing it
448
+ * as an unknown input (#1362). For instance, an Outlook `get-email-list` task
449
+ * saved at connector 1.47.0 carries
450
+ * `[{ name: 'bodyFormat', location: 'query', type: 'string' }]`.
451
+ *
452
+ * A carried field only adds a name: an input the library declares is checked
453
+ * against the library, and a carried field is never required.
454
+ */
455
+ carriedFields?: readonly CarriedConnectorField[];
456
+ }
428
457
  /**
429
458
  * The external agent's or automation's `executionType`, as Integration Service
430
459
  * reports it for the connector object.
@@ -1088,22 +1117,22 @@ declare class TaskBuilder {
1088
1117
  *
1089
1118
  * @param descriptor - The generated operation, from a prepared connector module.
1090
1119
  * @param inputs - The activity's inputs, typed by the descriptor.
1091
- * @param opts - Symbolic `connection` / `folder`, an action `version`, and the
1092
- * `object` a generic operation addresses.
1120
+ * @param opts - Symbolic `connection` / `folder`, an action `version`, the
1121
+ * `object` a generic operation addresses, and the `carriedFields` decompile writes.
1093
1122
  * @returns This builder, so calls chain.
1094
1123
  */
1095
- connector<I extends Record<string, unknown>, O>(descriptor: ConnectorDescriptor<I, O>, inputs: I, opts?: ConnectorOpts): this;
1124
+ connector<I extends Record<string, unknown>, O>(descriptor: ConnectorDescriptor<I, O>, inputs: I, opts?: CaseConnectorOpts): this;
1096
1125
  /**
1097
1126
  * Stringly form, for a connector with no prepared module.
1098
1127
  *
1099
1128
  * @param key - The connector library key, e.g. `'uipath-salesforce-slack'`.
1100
1129
  * @param action - The operation id, e.g. `'send-message-to-channel'`.
1101
1130
  * @param inputs - The activity's inputs.
1102
- * @param opts - Symbolic `connection` / `folder`, an action `version`, and the
1103
- * `object` a generic operation addresses.
1131
+ * @param opts - Symbolic `connection` / `folder`, an action `version`, the
1132
+ * `object` a generic operation addresses, and the `carriedFields` decompile writes.
1104
1133
  * @returns This builder, so calls chain.
1105
1134
  */
1106
- connector(key: string, action: string, inputs?: Record<string, unknown>, opts?: ConnectorOpts): this;
1135
+ connector(key: string, action: string, inputs?: Record<string, unknown>, opts?: CaseConnectorOpts): this;
1107
1136
  /**
1108
1137
  * A wait-for-connector task — suspend the stage until an Integration Service
1109
1138
  * **event** fires. A generated descriptor supplies the connector/event identity;
@@ -398,7 +398,12 @@ class TaskBuilder {
398
398
  this.setKind('connector');
399
399
  // Reuse the shared connector() so descriptor/nodeType splitting + version
400
400
  // handling match the Flow surface exactly; store the connector ActionSpec.
401
- this._connector = makeConnector(a, b, c, d);
401
+ const spec = makeConnector(a, b, c, d);
402
+ // The options are the 4th argument in the stringly form, the 3rd with a descriptor.
403
+ const carried = (typeof a === 'string' ? d : c)?.carriedFields;
404
+ if (carried !== undefined)
405
+ spec.carriedFields = carried;
406
+ this._connector = spec;
402
407
  return this;
403
408
  }
404
409
  waitForConnector(descriptorOrSpec = {}, opts) {
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A Designer-saved connector task's `optionalConfiguration`, kept through a
3
+ * decompile -> compile round trip (#1362).
4
+ *
5
+ * Studio Web saves a connector task's configuration in two halves:
6
+ * `essentialConfiguration` (what the runtime needs, which `compile` rebuilds
7
+ * from the library) and `optionalConfiguration` (the Designer's own state,
8
+ * including `fieldsContainer.inputFields`, the live connector's field list).
9
+ * Compile writes the essential half only, so before this a plan lost its saved
10
+ * field list on its first SDK compile. The next `decompile` then had nothing to
11
+ * carry `carriedFields` from, and `check` refused a field such as Outlook
12
+ * `bodyFormat` again on the second edit.
13
+ *
14
+ * Decompile now records the saved optional half in `preserveCaseJson`, keyed
15
+ * by task, and compile writes it back. It is written back only while the task
16
+ * still calls the same connector operation (connector key, object, method and
17
+ * path): a task the edit pointed at another operation has no saved state for
18
+ * that operation, and gets the essential half only, as a new task does.
19
+ *
20
+ * Only a half with a `fieldsContainer` is kept. Studio Web renders a task's
21
+ * fields from that container when the optional half is non-empty, and shows
22
+ * them empty without it (the essential-only rule in `core/connector-configuration.ts`).
23
+ *
24
+ * The half also holds the Designer's display caches (`cachedBrowserItems`,
25
+ * `cachedLookupValues`, `cachedPropertyMappings`), which describe input VALUES
26
+ * as they were saved: `cachedBrowserItems` maps `parentFolderId` to the folder
27
+ * "Inbox" and its id. After an edit that changes the value, that entry would
28
+ * describe the old folder. So decompile also records each cached field's saved
29
+ * value, and compile keeps a cache entry only when the field still has that
30
+ * value. A cache entry not in the `[field, item]` pair form, the only form
31
+ * measured in Designer plans, cannot be matched to a field and is dropped.
32
+ */
33
+ /** The connector operation a connector task calls, as its `context` rows name it. */
34
+ export interface ConnectorTaskIdentity {
35
+ connectorKey: string;
36
+ objectName: string;
37
+ method: string;
38
+ path: string;
39
+ }
40
+ /** The operation an `execute-connector-activity` task calls, or `undefined` for any other task. */
41
+ export declare function connectorTaskIdentity(task: Record<string, any>): ConnectorTaskIdentity | undefined;
42
+ /** Whether two operations are the same one. */
43
+ export declare function sameConnectorOperation(a: ConnectorTaskIdentity, b: ConnectorTaskIdentity): boolean;
44
+ /** The task's saved `optionalConfiguration`, when it is an object with a `fieldsContainer`. */
45
+ export declare function savedOptionalConfiguration(task: Record<string, any>): Record<string, unknown> | undefined;
46
+ /**
47
+ * Write `optional` into the task's configuration beside its essential half.
48
+ *
49
+ * @returns `'written'`, `'same'` when the task already has that exact half,
50
+ * `'different'` when it has another one, or `'no-configuration'` when the task
51
+ * has no configuration to write into.
52
+ */
53
+ export declare function writeOptionalConfiguration(task: Record<string, any>, optional: Record<string, unknown>): 'written' | 'same' | 'different' | 'no-configuration';
54
+ /**
55
+ * The saved value of every field the half's caches name, as the task sends it.
56
+ * Decompile records it; compile compares against it (see the module comment).
57
+ */
58
+ export declare function cachedFieldValues(task: Record<string, any>, optional: Record<string, unknown>): Record<string, unknown>;
59
+ /**
60
+ * `optional` with each cache entry kept only while its field still has its
61
+ * saved value in `task`. A cache that is not a list keeps its value; an entry
62
+ * not in the `[field, item]` form is dropped.
63
+ */
64
+ export declare function withCurrentCaches(task: Record<string, any>, optional: Record<string, unknown>, saved?: Readonly<Record<string, unknown>>): Record<string, unknown>;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * A Designer-saved connector task's `optionalConfiguration`, kept through a
3
+ * decompile -> compile round trip (#1362).
4
+ *
5
+ * Studio Web saves a connector task's configuration in two halves:
6
+ * `essentialConfiguration` (what the runtime needs, which `compile` rebuilds
7
+ * from the library) and `optionalConfiguration` (the Designer's own state,
8
+ * including `fieldsContainer.inputFields`, the live connector's field list).
9
+ * Compile writes the essential half only, so before this a plan lost its saved
10
+ * field list on its first SDK compile. The next `decompile` then had nothing to
11
+ * carry `carriedFields` from, and `check` refused a field such as Outlook
12
+ * `bodyFormat` again on the second edit.
13
+ *
14
+ * Decompile now records the saved optional half in `preserveCaseJson`, keyed
15
+ * by task, and compile writes it back. It is written back only while the task
16
+ * still calls the same connector operation (connector key, object, method and
17
+ * path): a task the edit pointed at another operation has no saved state for
18
+ * that operation, and gets the essential half only, as a new task does.
19
+ *
20
+ * Only a half with a `fieldsContainer` is kept. Studio Web renders a task's
21
+ * fields from that container when the optional half is non-empty, and shows
22
+ * them empty without it (the essential-only rule in `core/connector-configuration.ts`).
23
+ *
24
+ * The half also holds the Designer's display caches (`cachedBrowserItems`,
25
+ * `cachedLookupValues`, `cachedPropertyMappings`), which describe input VALUES
26
+ * as they were saved: `cachedBrowserItems` maps `parentFolderId` to the folder
27
+ * "Inbox" and its id. After an edit that changes the value, that entry would
28
+ * describe the old folder. So decompile also records each cached field's saved
29
+ * value, and compile keeps a cache entry only when the field still has that
30
+ * value. A cache entry not in the `[field, item]` pair form, the only form
31
+ * measured in Designer plans, cannot be matched to a field and is dropped.
32
+ */
33
+ const JSON_STRING = '=jsonString:';
34
+ const IDENTITY_ROWS = ['connectorKey', 'objectName', 'method', 'path'];
35
+ /** The task's `metadata` context row's body, when it has one. */
36
+ function metadataBody(task) {
37
+ const rows = task?.data?.context;
38
+ if (!Array.isArray(rows))
39
+ return undefined;
40
+ const body = rows.find((row) => row?.name === 'metadata')?.body;
41
+ return body && typeof body === 'object' ? body : undefined;
42
+ }
43
+ /** The parsed `=jsonString:` configuration of a connector task, or `undefined`. */
44
+ function parsedConfiguration(task) {
45
+ const encoded = metadataBody(task)?.activityPropertyConfiguration?.configuration;
46
+ if (typeof encoded !== 'string' || !encoded.startsWith(JSON_STRING))
47
+ return undefined;
48
+ try {
49
+ const parsed = JSON.parse(encoded.slice(JSON_STRING.length));
50
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ /** The operation an `execute-connector-activity` task calls, or `undefined` for any other task. */
57
+ export function connectorTaskIdentity(task) {
58
+ if (task?.type !== 'execute-connector-activity' || !Array.isArray(task.data?.context))
59
+ return undefined;
60
+ const value = (name) => task.data.context.find((row) => row?.name === name)?.value;
61
+ const values = IDENTITY_ROWS.map(value);
62
+ if (values.some((v) => typeof v !== 'string'))
63
+ return undefined;
64
+ const [connectorKey, objectName, method, path] = values;
65
+ return { connectorKey, objectName, method, path };
66
+ }
67
+ /** Whether two operations are the same one. */
68
+ export function sameConnectorOperation(a, b) {
69
+ return IDENTITY_ROWS.every((name) => a[name] === b[name]);
70
+ }
71
+ /** The task's saved `optionalConfiguration`, when it is an object with a `fieldsContainer`. */
72
+ export function savedOptionalConfiguration(task) {
73
+ const optional = parsedConfiguration(task)?.optionalConfiguration;
74
+ if (!optional || typeof optional !== 'object' || Array.isArray(optional))
75
+ return undefined;
76
+ const container = optional.fieldsContainer;
77
+ return container && typeof container === 'object' ? optional : undefined;
78
+ }
79
+ /**
80
+ * Write `optional` into the task's configuration beside its essential half.
81
+ *
82
+ * @returns `'written'`, `'same'` when the task already has that exact half,
83
+ * `'different'` when it has another one, or `'no-configuration'` when the task
84
+ * has no configuration to write into.
85
+ */
86
+ export function writeOptionalConfiguration(task, optional) {
87
+ const config = parsedConfiguration(task);
88
+ if (!config)
89
+ return 'no-configuration';
90
+ if (config.optionalConfiguration !== undefined)
91
+ return JSON.stringify(config.optionalConfiguration) === JSON.stringify(optional) ? 'same' : 'different';
92
+ metadataBody(task).activityPropertyConfiguration.configuration =
93
+ `${JSON_STRING}${JSON.stringify({ ...config, optionalConfiguration: optional })}`;
94
+ return 'written';
95
+ }
96
+ /** Whether a configuration key is one of the Designer's value caches (`cachedBrowserItems`, …). */
97
+ function isCacheKey(key) {
98
+ return /^cached[A-Z]/.test(key);
99
+ }
100
+ /** A cache entry's field, when it is in the `[field, item]` pair form. */
101
+ function cacheEntryField(entry) {
102
+ return Array.isArray(entry) && entry.length === 2 && typeof entry[0] === 'string' ? entry[0] : undefined;
103
+ }
104
+ function camel(name) {
105
+ return name.replace(/_([a-z0-9])/gi, (_m, c) => String(c).toUpperCase());
106
+ }
107
+ /** The input container `name` of a task (`pathParameters`, `queryParameters`, `body`). */
108
+ function container(task, name) {
109
+ const body = Array.isArray(task?.data?.inputs)
110
+ ? task.data.inputs.find((row) => row?.name === name)?.body : undefined;
111
+ return body && typeof body === 'object' && !Array.isArray(body) ? body : undefined;
112
+ }
113
+ /**
114
+ * The wire value a task sends for `field`, or `null` when it sends none: a
115
+ * query or path parameter (by its name or the camelCased wire name), else a
116
+ * body field (a dotted name reads the nested body).
117
+ */
118
+ function fieldValue(task, field) {
119
+ for (const name of ['queryParameters', 'pathParameters']) {
120
+ const slot = container(task, name);
121
+ for (const key of [field, camel(field)])
122
+ if (slot && Object.prototype.hasOwnProperty.call(slot, key))
123
+ return slot[key];
124
+ }
125
+ let at = container(task, 'body');
126
+ if (at && Object.prototype.hasOwnProperty.call(at, field))
127
+ return at[field];
128
+ for (const part of field.split('.')) {
129
+ if (!at || typeof at !== 'object' || !Object.prototype.hasOwnProperty.call(at, part))
130
+ return null;
131
+ at = at[part];
132
+ }
133
+ return at ?? null;
134
+ }
135
+ /**
136
+ * The saved value of every field the half's caches name, as the task sends it.
137
+ * Decompile records it; compile compares against it (see the module comment).
138
+ */
139
+ export function cachedFieldValues(task, optional) {
140
+ const out = {};
141
+ for (const [key, cache] of Object.entries(optional)) {
142
+ if (!isCacheKey(key) || !Array.isArray(cache))
143
+ continue;
144
+ for (const entry of cache) {
145
+ const field = cacheEntryField(entry);
146
+ if (field !== undefined && !Object.prototype.hasOwnProperty.call(out, field))
147
+ out[field] = fieldValue(task, field);
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+ /**
153
+ * `optional` with each cache entry kept only while its field still has its
154
+ * saved value in `task`. A cache that is not a list keeps its value; an entry
155
+ * not in the `[field, item]` form is dropped.
156
+ */
157
+ export function withCurrentCaches(task, optional, saved = {}) {
158
+ const same = (field) => Object.prototype.hasOwnProperty.call(saved, field)
159
+ && JSON.stringify(saved[field]) === JSON.stringify(fieldValue(task, field));
160
+ return Object.fromEntries(Object.entries(optional).map(([key, cache]) => {
161
+ if (!isCacheKey(key) || !Array.isArray(cache))
162
+ return [key, cache];
163
+ return [key, cache.filter((entry) => {
164
+ const field = cacheEntryField(entry);
165
+ return field !== undefined && same(field);
166
+ })];
167
+ }));
168
+ }
@@ -4,6 +4,7 @@
4
4
  * input bytes; platform-owned preservation lanes are handled separately.
5
5
  */
6
6
  import type { Library } from '../core/library.js';
7
+ import { type ConnectorTaskIdentity } from './connector-saved-config.js';
7
8
  interface IoRow {
8
9
  id?: string;
9
10
  name?: string;
@@ -217,6 +218,14 @@ type Preservation = {
217
218
  task: string;
218
219
  fields: Record<string, unknown>;
219
220
  }>;
221
+ connectorConfigurations?: Array<{
222
+ stage: string;
223
+ lane: number;
224
+ task: string;
225
+ connector: ConnectorTaskIdentity;
226
+ optionalConfiguration: Record<string, unknown>;
227
+ cachedFieldValues: Record<string, unknown>;
228
+ }>;
220
229
  triggerDataFields?: Array<Record<string, unknown>>;
221
230
  omitStagePositions?: string[];
222
231
  omitTaskElementIds?: Array<{
@@ -1,3 +1,5 @@
1
+ import { savedFieldsMissingFromLibrary, withCarriedFields } from '../core/carried-connector-fields.js';
2
+ import { cachedFieldValues, connectorTaskIdentity, savedOptionalConfiguration } from './connector-saved-config.js';
1
3
  import { commentText } from '../core/comment-text.js';
2
4
  import { implicitInArgTrigger } from './in-arg-binding.js';
3
5
  import { tsLiteral, tsNumber } from '../core/ts-literal.js';
@@ -511,6 +513,21 @@ function selectNextStageByExit(plan, refs) {
511
513
  ['connector', connectorSource],
512
514
  ])]]);
513
515
  }
516
+ /**
517
+ * A connector task's parsed `=jsonString:` activity configuration, or
518
+ * `undefined` when it has none. Malformed JSON is `connectorVersion`'s to refuse.
519
+ */
520
+ function savedConfiguration(context) {
521
+ const encoded = context.get('metadata')?.body?.activityPropertyConfiguration?.configuration;
522
+ if (typeof encoded !== 'string' || !encoded.startsWith('=jsonString:'))
523
+ return undefined;
524
+ try {
525
+ return JSON.parse(encoded.slice('=jsonString:'.length));
526
+ }
527
+ catch {
528
+ return undefined;
529
+ }
530
+ }
514
531
  function connectorVersion(context) {
515
532
  const metadata = context.get('metadata')?.body;
516
533
  const encoded = metadata?.activityPropertyConfiguration?.configuration;
@@ -555,6 +572,11 @@ function connectorInputs(slots, resolved, owner) {
555
572
  const parameterByWireName = new Map();
556
573
  for (const parameter of resolved.entry.operation?.parameters ?? [])
557
574
  parameterByWireName.set(toCamel(String(parameter.name)), String(parameter.name));
575
+ // A carried query or path field is a parameter too, which the library's
576
+ // `operation.parameters` does not list.
577
+ for (const field of resolved.fields)
578
+ if ((field.location === 'query' || field.location === 'path') && !parameterByWireName.has(toCamel(field.name)))
579
+ parameterByWireName.set(toCamel(field.name), field.name);
558
580
  const inputs = {};
559
581
  const fileParts = new Set(resolved.multipartParts
560
582
  ?.filter((part) => part.dataType.toLowerCase() === 'file').map((part) => part.name));
@@ -604,7 +626,13 @@ function connectorCall(task, refs, library) {
604
626
  objectName: String(objectName),
605
627
  version: connectorVersion(context),
606
628
  });
607
- const resolved = library.resolve(identity.key, identity.action, identity.version, identity.object);
629
+ const fromLibrary = library.resolve(identity.key, identity.action, identity.version, identity.object);
630
+ // The fields the plan's saved field list declares and the library lacks: a
631
+ // Designer plan saved against a newer connector than the library uses them,
632
+ // and the source carries their declarations so check and compile accept the
633
+ // values instead of refusing them as unknown inputs (#1362).
634
+ const carried = savedFieldsMissingFromLibrary(savedConfiguration(context), fromLibrary);
635
+ const resolved = withCarriedFields(fromLibrary, carried);
608
636
  const inputs = connectorInputs(Array.isArray(data.inputs) ? data.inputs : [], resolved);
609
637
  const connection = bindingName(context.get('connection')?.value, refs);
610
638
  const folder = bindingName(context.get('folderKey')?.value, refs);
@@ -613,6 +641,7 @@ function connectorCall(task, refs, library) {
613
641
  ['folder', folder !== undefined ? tsLiteral(folder) : undefined],
614
642
  ['version', identity.version !== '1.0.0' ? tsLiteral(identity.version) : undefined],
615
643
  ['object', identity.object !== undefined ? tsLiteral(identity.object) : undefined],
644
+ ['carriedFields', carried.length ? tsLiteral(carried) : undefined],
616
645
  ]);
617
646
  return `.connector(${tsLiteral(identity.key)}, ${tsLiteral(identity.action)}, ${tsLiteral(inputs)}${options ? `, ${options}` : ''})`;
618
647
  }
@@ -1473,6 +1502,7 @@ export function collectPreservation(plan) {
1473
1502
  'entryConditions', 'conditions', 'nonSequentialRunConditions',
1474
1503
  ]);
1475
1504
  const taskFields = [];
1505
+ const connectorConfigurations = [];
1476
1506
  const omitTaskElementIds = [];
1477
1507
  const omitStagePositions = [];
1478
1508
  for (const stage of (plan.nodes ?? []).filter(isStage)) {
@@ -1494,10 +1524,22 @@ export function collectPreservation(plan) {
1494
1524
  throw new Error(`task "${task.displayName ?? task.id ?? '?'}" has unsupported field "${field}", which is not allowed in the collision-refusing preservation channel.`);
1495
1525
  if (Object.keys(fields).length)
1496
1526
  taskFields.push({ stage: label, lane: laneIndex, task: task.displayName ?? '', fields });
1527
+ // The Designer's half of a connector task's configuration, which compile
1528
+ // does not rebuild: its saved field list is what the next decompile
1529
+ // carries `carriedFields` from (#1362).
1530
+ const connector = connectorTaskIdentity(task);
1531
+ const optionalConfiguration = connector && savedOptionalConfiguration(task);
1532
+ if (connector && optionalConfiguration)
1533
+ connectorConfigurations.push({
1534
+ stage: label, lane: laneIndex, task: task.displayName ?? '', connector, optionalConfiguration,
1535
+ cachedFieldValues: cachedFieldValues(task, optionalConfiguration),
1536
+ });
1497
1537
  }));
1498
1538
  }
1499
1539
  if (taskFields.length)
1500
1540
  preservation.taskFields = taskFields;
1541
+ if (connectorConfigurations.length)
1542
+ preservation.connectorConfigurations = connectorConfigurations;
1501
1543
  if (omitTaskElementIds.length)
1502
1544
  preservation.omitTaskElementIds = omitTaskElementIds;
1503
1545
  if (omitStagePositions.length)
@@ -27,6 +27,26 @@ export interface PreservedTaskRef {
27
27
  /** Task display name used to find the rebuilt task. */
28
28
  task: string;
29
29
  }
30
+ /**
31
+ * A Designer-saved connector task's `optionalConfiguration`, written back while
32
+ * the task calls the same operation (`case/connector-saved-config.ts`, #1362).
33
+ */
34
+ export interface PreservedConnectorConfiguration extends PreservedTaskRef {
35
+ /** The operation the half was saved for: the task's `connectorKey`, `objectName`, `method` and `path`. */
36
+ connector: {
37
+ connectorKey: string;
38
+ objectName: string;
39
+ method: string;
40
+ path: string;
41
+ };
42
+ /** The saved half, verbatim. It always has a `fieldsContainer`. */
43
+ optionalConfiguration: Record<string, unknown>;
44
+ /**
45
+ * The saved value of each field the half's caches name. Compile keeps a cache
46
+ * entry only while its field still has this value.
47
+ */
48
+ cachedFieldValues?: Record<string, unknown>;
49
+ }
30
50
  export interface PreservedTaskFields extends PreservedTaskRef {
31
51
  /** Wire-only fields; the preservation contract permits `elementId` only. */
32
52
  fields: Record<string, unknown>;
@@ -129,6 +149,8 @@ export interface CaseJsonPreservation {
129
149
  variableOrder?: Partial<Record<PreservedVariableLane, string[]>>;
130
150
  /** Wire-only task identity fields aligned without authoring them. */
131
151
  taskFields?: PreservedTaskFields[];
152
+ /** Designer-saved connector `optionalConfiguration`s, by task. */
153
+ connectorConfigurations?: PreservedConnectorConfiguration[];
132
154
  /** Platform-owned trigger data fields aligned by trigger order. */
133
155
  triggerDataFields?: Array<Record<string, unknown>>;
134
156
  /** Stage labels whose source JSON intentionally omitted inline positions. */
@@ -164,6 +164,20 @@ export function caseJsonPreservationErrors(payload) {
164
164
  errors.push(`preserved binding row "${r.id}" carries no resource key other than its own id`);
165
165
  });
166
166
  }
167
+ for (const entry of payload.connectorConfigurations ?? []) {
168
+ if (!Number.isInteger(entry.lane) || entry.lane < 0)
169
+ errors.push(`preserved connector configuration lane "${entry.lane}" is not a non-negative integer`);
170
+ const connector = entry.connector;
171
+ if (!connector || ['connectorKey', 'objectName', 'method', 'path'].some((name) => typeof connector[name] !== 'string'))
172
+ errors.push(`preserved connector configuration of task "${entry.task}" must name its connectorKey, objectName, method and path`);
173
+ const optional = entry.optionalConfiguration;
174
+ if (!optional || typeof optional !== 'object' || Array.isArray(optional)
175
+ || !optional.fieldsContainer || typeof optional.fieldsContainer !== 'object')
176
+ errors.push(`preserved connector configuration of task "${entry.task}" must be an object with a fieldsContainer`);
177
+ if (entry.cachedFieldValues !== undefined
178
+ && (!entry.cachedFieldValues || typeof entry.cachedFieldValues !== 'object' || Array.isArray(entry.cachedFieldValues)))
179
+ errors.push(`preserved connector configuration of task "${entry.task}" has cachedFieldValues that is not an object`);
180
+ }
167
181
  for (const [index, fields] of (payload.triggerDataFields ?? []).entries())
168
182
  for (const field of Object.keys(fields))
169
183
  if (!PRESERVED_TRIGGER_DATA_KEYS.has(field))
@@ -13,6 +13,8 @@ import { isUnrestorableSlotId, taskFingerprint } from './preserve.js';
13
13
  import { v31TaskSets } from './task-sets.js';
14
14
  import { camelizeName, defaultActionOutputVar } from './action-output-var.js';
15
15
  import { buildConnectorInputs, nestConnectorBody, replayInputsOf, transportHttpMethod } from '../core/connector-inputs.js';
16
+ import { carriedConnectorFieldsRefusal, withCarriedFields } from '../core/carried-connector-fields.js';
17
+ import { connectorTaskIdentity, sameConnectorOperation, withCurrentCaches, writeOptionalConfiguration } from './connector-saved-config.js';
16
18
  import { buildEssentialConfiguration } from '../core/connector-configuration.js';
17
19
  import { EQUALS_EXPR_DIALECT } from '../core/connector-checks.js';
18
20
  import { bindingLabelRefusal } from '../core/binding-messages.js';
@@ -612,6 +614,20 @@ export function applyPreservation(result, preservation, options = {}) {
612
614
  task[field] = value;
613
615
  }
614
616
  }
617
+ for (const entry of preservation.connectorConfigurations ?? []) {
618
+ const stage = stageNodes.find((candidate) => candidate.data?.label === entry.stage);
619
+ const task = stageTask(stage, entry.task);
620
+ // A deleted task, or one the edit pointed at another operation, has no use
621
+ // for the saved half: it gets the essential half only, as a new task does.
622
+ const now = task && connectorTaskIdentity(task);
623
+ if (!task || !now || !sameConnectorOperation(now, entry.connector))
624
+ continue;
625
+ // The Designer's caches describe input values as saved: an entry whose
626
+ // field the edit changed would show the old value, so it is dropped.
627
+ const written = writeOptionalConfiguration(task, withCurrentCaches(task, entry.optionalConfiguration, entry.cachedFieldValues));
628
+ if (written === 'different' && !options.overwrite)
629
+ throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved connector configuration of task "${entry.stage}/${entry.task}" collides with serializer-owned output.`);
630
+ }
615
631
  for (const entry of preservation.omitTaskElementIds ?? []) {
616
632
  const stage = stageNodes.find((candidate) => candidate.data?.label === entry.stage);
617
633
  const task = stageTask(stage, entry.task);
@@ -796,6 +812,7 @@ export function followRenames(result, preservation) {
796
812
  stages,
797
813
  },
798
814
  ...(preservation.taskFields ? { taskFields: preservation.taskFields.map(ref) } : {}),
815
+ ...(preservation.connectorConfigurations ? { connectorConfigurations: preservation.connectorConfigurations.map(ref) } : {}),
799
816
  ...(preservation.omitTaskElementIds ? { omitTaskElementIds: preservation.omitTaskElementIds.map(ref) } : {}),
800
817
  ...(preservation.omitStagePositions ? { omitStagePositions: preservation.omitStagePositions.map(stageName) } : {}),
801
818
  };
@@ -1901,7 +1918,13 @@ function connectorTaskData(spec, resolver, elementId, where, executionType, even
1901
1918
  if (!resolver.library)
1902
1919
  throw new CompileRefusal('LIBRARY_REQUIRED', `connector task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
1903
1920
  const library = resolver.library;
1904
- const rc = onTask(where, () => resolveOperation(library, spec));
1921
+ // A decompiled task's carried fields widen the library's field list with the
1922
+ // plan's own declarations, so a field the Designer saved against a newer
1923
+ // connector than the library compiles instead of being refused (#1362).
1924
+ const carriedRefusal = carriedConnectorFieldsRefusal(spec.carriedFields, where);
1925
+ if (carriedRefusal)
1926
+ throw new CompileRefusal('CARRIED_DEFINITION_INVALID', carriedRefusal);
1927
+ const rc = withCarriedFields(onTask(where, () => resolveOperation(library, spec)), spec.carriedFields);
1905
1928
  const op = rc.entry.operation ?? {};
1906
1929
  const context = [
1907
1930
  io('connectorKey', 'string', { value: rc.connectorKey }),