@uipath/maestro-builder-sdk 6.69.1 → 6.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,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) {
@@ -5,7 +5,7 @@
5
5
  * the library does not carry passed `check` and failed at `compile` (#1014).
6
6
  * Flow and BPMN read their connector facts from `connectorCheckOpts`
7
7
  * (`core/cli-library.ts`); Case now does too, through this one builder, which
8
- * both `case check` and `case compile`'s check pass call.
8
+ * `case check` calls.
9
9
  */
10
10
  import { connectorCheckOpts } from '../core/cli-library.js';
11
11
  /**
@@ -5,22 +5,24 @@
5
5
  * Usage:
6
6
  * node --experimental-strip-types compile-cli.js <case.ts | BaseName> [-o out.json]
7
7
  *
8
- * Runs the fast static check first; if it passes, serializes to a V32
9
- * caseplan.json. The authoritative deploy gate is `uip maestro case validate`.
8
+ * Only serializes, to a V32 caseplan.json (docs/CHECK_COMPILE_CONTRACT_DESIGN.md
9
+ * D1, phase 8). It does not run `check()`'s lint rules: run `case check` first
10
+ * for those. It stops only when it cannot write a correct plan, a refusal the
11
+ * serializer throws, printed in `check`'s `✗ [CODE]` shape (and `case check`
12
+ * reports the same refusal, from the same code). The authoritative deploy gate
13
+ * is `uip maestro case validate`.
10
14
  * Output defaults to the plan file the project in the working directory ships
11
15
  * (`caseplan.case` for a project `uip maestro case init` scaffolded), else
12
16
  * `caseplan.json`. An `-o` that names a different plan file than the project
13
17
  * ships is refused (CASE_OUTPUT_NOT_SHIPPED), and so is a plan file written into
14
18
  * a solution directory above its Case project (CASE_OUTPUT_OUTSIDE_PROJECT).
15
19
  */
16
- import { reportDiagnostics, reportResult, thrownDiagnostic } from '../cli-result.js';
20
+ import { diagnosticLine, reportDiagnostics, reportResult, thrownDiagnostic } from '../cli-result.js';
17
21
  import { writeJsonFile } from '../core/json-file.js';
18
22
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
19
23
  import { join, dirname, basename, resolve } from 'node:path';
20
24
  import { resolveCaseFile, loadBuiltCase } from './load.js';
21
25
  import { serialize } from './serialize.js';
22
- import { check } from './check.js';
23
- import { caseCheckOpts } from './cli-library.js';
24
26
  import { entryPointIO } from './entry-points.js';
25
27
  import { Bindings } from '../core/bindings.js';
26
28
  import { offlineBindingLabel } from './ids.js';
@@ -32,6 +34,7 @@ import { CASE_FORMAT_PROFILE } from './format-profile.js';
32
34
  import { deriveCaseBindingsV2 } from './bindings-v2.js';
33
35
  import { BINDINGS_V2_DOCUMENT_VERSION } from '../core/bindings-v2-resource.js';
34
36
  import { sha256Uuid } from '../core/stable-id.js';
37
+ import { CompileRefusal } from '../core/compile-refusal.js';
35
38
  export async function run(argv) {
36
39
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
37
40
  console.error('usage: compile <case.ts | BaseName> [-o <project>/caseplan.case]');
@@ -53,23 +56,13 @@ export async function run(argv) {
53
56
  // fine right after a pull on Flow failed here for no visible reason. The helper
54
57
  // also auto-detects the `connectors-local/` overlay `registry prepare` writes,
55
58
  // which is what the three copies of these lines used to each decide for itself.
56
- // Opened BEFORE the check pass, which reads the same library (#1014).
59
+ // `case check` opens it the same way (#1014).
57
60
  const library = openCliLibrary(argv);
58
61
  const { out, bindings } = caseCompileTarget(argv);
59
- const diags = check(built, caseCheckOpts(library, bindings));
60
- for (const d of diags)
61
- console.error(` ${d.level === 'error' ? '✗' : '⚠'} [${d.code}] ${d.message}`);
62
- // Reported before serialize, so every exit path carries what stderr printed.
63
- reportDiagnostics(diags);
64
- const errors = diags.filter((d) => d.level === 'error');
65
- if (errors.length) {
66
- console.error(`compile: ${errors.length} error(s); not emitting.`);
67
- return 1;
68
- }
69
62
  const shadow = shadowedOutputDiagnostic(out);
70
63
  if (shadow) {
71
64
  console.error(` ✗ [${shadow.code}] ${shadow.message} Fix: ${shadow.suggestion}.`);
72
- reportDiagnostics([...diags, shadow]);
65
+ reportDiagnostics([shadow]);
73
66
  console.error('compile: 1 error(s); not emitting.');
74
67
  return 1;
75
68
  }
@@ -78,9 +71,28 @@ export async function run(argv) {
78
71
  caseplan = serialize(built, { library, bindings });
79
72
  }
80
73
  catch (error) {
81
- reportDiagnostics([...diags, thrownDiagnostic(error)]);
82
- throw error;
74
+ if (!(error instanceof Error) || (!(error instanceof CompileRefusal) && error.message.startsWith('internal SDK error:'))) {
75
+ // A broken SDK invariant still ends the run through the caller (exit 1,
76
+ // `--json` `error`); the diagnostic is reported first.
77
+ reportDiagnostics([thrownDiagnostic(error)]);
78
+ throw error;
79
+ }
80
+ // The serializer cannot write a correct plan: a coded refusal, or a throw
81
+ // it has no refusal for (a `TypeError` on a malformed spec), which is
82
+ // COMPILE_FAILED, as `case check` reports it (`refusalOf`, serialize.ts).
83
+ const refused = error instanceof CompileRefusal
84
+ ? {
85
+ level: 'error', code: error.code, message: error.message,
86
+ ...(error.suggestion !== undefined ? { suggestion: error.suggestion } : {}),
87
+ }
88
+ : { level: 'error', code: 'COMPILE_FAILED', message: error.message };
89
+ console.error(` ${diagnosticLine(refused)}`);
90
+ reportDiagnostics([refused]);
91
+ console.error('compile: 1 error(s); not emitting.');
92
+ return 1;
83
93
  }
94
+ // Reported before the first write, so a write that throws still carries it.
95
+ reportDiagnostics([]);
84
96
  // Entry points first: each trigger carries its entry point's `uniqueId` as
85
97
  // `data.inputs.entryPointId`, so the plan is written after the ids are known.
86
98
  const triggers = syncEntryPoints(caseplan, out);
@@ -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))