@tenonhq/dovetail-servicenow 0.0.34 → 0.0.36

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.
package/README.md CHANGED
@@ -93,6 +93,13 @@ npx dove-sn add-choices \
93
93
 
94
94
  # JSON payload form (recommended for >5 choices)
95
95
  npx dove-sn add-choices --from-json ./choices.json
96
+
97
+ # Retire values — a SOFT delete (sets inactive=true), never a row drop
98
+ npx dove-sn remove-choices \
99
+ --table x_cadso_core_event \
100
+ --column state \
101
+ --update-set 0083c3bb33d003507b18bc534d5c7b6d \
102
+ --values "expired,failed"
96
103
  ```
97
104
 
98
105
  JSON payload shape:
@@ -110,10 +117,37 @@ JSON payload shape:
110
117
  }
111
118
  ```
112
119
 
120
+ `remove-choices` takes the same `--from-json` form, with a `values` array in
121
+ place of `choices`.
122
+
123
+ ### Removal semantics
124
+
125
+ - **Soft delete.** Sets `sys_choice.inactive = true`; the row is retained and
126
+ the operation is reversible by re-adding the value. `sys_dictionary.choice` is
127
+ left alone — retiring values does not un-make the column a choice field.
128
+ - **Idempotent.** `deactivated` (a live row was flipped) / `unchanged` (already
129
+ inactive) / `missing` (no such value on the field). Re-running writes nothing.
130
+ - **Duplicates.** `sys_choice` has no uniqueness constraint on
131
+ `(name, element, value, language)`, so a field can hold several live rows for
132
+ one value. Every live row is deactivated, and `sysIds` lists all of them — a
133
+ length above 1 is your signal the field needs cleaning up.
134
+ - **Inherited choices are not covered.** Matching is scoped to
135
+ `sys_choice.name = <table>`, so a value defined on a *parent* table reports
136
+ `missing` rather than being deactivated. Hiding one on a child table needs an
137
+ override row this verb does not write.
138
+ - **Promotion.** ServiceNow captures the change as a single `Choice list` record
139
+ for the whole column, not one per value — so promoting the update set moves
140
+ the entire choice-list state for that column.
141
+
113
142
  ## Programmatic
114
143
 
115
144
  ```ts
116
- import { createClient, addChoicesToField } from "@tenonhq/dovetail-servicenow";
145
+ import {
146
+ createClient,
147
+ addChoicesToField,
148
+ removeChoicesFromField,
149
+ ChoiceWriteError,
150
+ } from "@tenonhq/dovetail-servicenow";
117
151
 
118
152
  var client = createClient({});
119
153
  var result = await addChoicesToField(client, { /* ... */ });
@@ -123,6 +157,32 @@ console.log(result.choices);
123
157
  // { value: "delivered", label: "Delivered", sysId: "...", action: "created" },
124
158
  // { value: "failed", label: "Failed", sysId: "...", action: "created" }
125
159
  // ]
160
+
161
+ var removed = await removeChoicesFromField(client, {
162
+ table: "x_cadso_core_event",
163
+ column: "state",
164
+ updateSetSysId: "0083c3bb33d003507b18bc534d5c7b6d",
165
+ values: ["expired"],
166
+ });
167
+ // removed.choices[0] -> { value: "expired", sysId: "...", sysIds: ["..."], action: "deactivated" }
168
+ ```
169
+
170
+ Both verbs write one value at a time. If a write fails partway through they
171
+ throw a **`ChoiceWriteError`** carrying `completed` (the values that already
172
+ landed, in result shape), `failedValue` (the one that threw — its state on the
173
+ instance is unknown), and `cause` (the original error). Catch it rather than
174
+ re-querying the instance to work out how far the run got:
175
+
176
+ ```ts
177
+ try {
178
+ await removeChoicesFromField(client, params);
179
+ } catch (e) {
180
+ if (e instanceof ChoiceWriteError) {
181
+ console.error("already deactivated:", e.completed.map((r) => r.value));
182
+ console.error("verify by hand:", e.failedValue);
183
+ }
184
+ throw e;
185
+ }
126
186
  ```
127
187
 
128
188
  ## Form, list & view layouts
package/dist/choices.d.ts CHANGED
@@ -8,7 +8,29 @@
8
8
  * so choices stay in the same application as the field.
9
9
  */
10
10
  import type { ServiceNowClient } from "./client";
11
- import type { AddChoicesParams, AddChoicesResult } from "./types";
11
+ import type { AddChoicesParams, AddChoicesResult, ChoiceActionResult, ChoiceRemovalResult, RemoveChoicesParams, RemoveChoicesResult } from "./types";
12
+ /**
13
+ * Thrown when a choices write fails partway through the value loop.
14
+ *
15
+ * Both verbs write one value at a time. Without this, a throw on value 3 of 5
16
+ * unwound the whole call and took the local `results` array with it — the
17
+ * caller could not tell which values had already been deactivated (or which
18
+ * sys_choice rows had already been created on the add path) without going and
19
+ * querying the instance. That is the worst possible moment to lose the record:
20
+ * the writes already landed and are already captured in the update set.
21
+ *
22
+ * `completed` holds the values that finished before the failure, in order, in
23
+ * exactly the shape they would have had in a successful result. `failedValue`
24
+ * is the value being processed when the write threw, and is NOT in `completed`
25
+ * — its state on the instance is unknown (the row may or may not have been
26
+ * written before the error), so it needs checking by hand.
27
+ */
28
+ export declare class ChoiceWriteError extends Error {
29
+ readonly completed: Array<ChoiceActionResult | ChoiceRemovalResult>;
30
+ readonly failedValue: string;
31
+ readonly cause: unknown;
32
+ constructor(verb: string, failedValue: string, completed: Array<ChoiceActionResult | ChoiceRemovalResult>, cause: unknown);
33
+ }
12
34
  export declare function encodeQueryValue(v: string): string;
13
35
  /**
14
36
  * Upsert choices for a field and (optionally) toggle sys_dictionary.choice.
@@ -16,3 +38,33 @@ export declare function encodeQueryValue(v: string): string;
16
38
  * for every row and skips the dictionary write when no change is required.
17
39
  */
18
40
  export declare function addChoicesToField(client: ServiceNowClient, params: AddChoicesParams): Promise<AddChoicesResult>;
41
+ /**
42
+ * Soft-delete choice values for a field: set `inactive=true` on each matching
43
+ * sys_choice row via pushWithUpdateSet. NEVER a hard delete — the row stays, so the
44
+ * change is reversible and the historical value still resolves on records that
45
+ * already hold it. (A hard drop of sys_choice is deliberately deferred; see DEV-511.)
46
+ *
47
+ * Idempotent:
48
+ * - active value -> "deactivated" (every live row for it flipped to inactive —
49
+ * usually one write, but more when the field holds duplicates)
50
+ * - already inactive -> "unchanged" (no write)
51
+ * - value not found -> "missing" (no write)
52
+ *
53
+ * The dictionary row is fetched only to PROVE the field exists — without that guard a
54
+ * mistyped column reports every value as "missing", the silent-failure this family
55
+ * exists to catch. sys_dictionary.choice is left alone on purpose: removing values
56
+ * does not un-make the column a choice field.
57
+ *
58
+ * Repeated values are collapsed before the loop, so `["a", "a"]` costs no more than
59
+ * `["a"]` and yields one result row — an idempotent verb must not depend on the caller
60
+ * de-duplicating first. (That is "no EXTRA writes from repeats", not "exactly one
61
+ * write": a single value still takes one write per live duplicate row on the field.)
62
+ *
63
+ * LIMITATION — inherited choices. Matching is scoped to `sys_choice.name = <table>`, so
64
+ * a value defined on a PARENT table (task.state inherited by a child) reports "missing"
65
+ * rather than being deactivated. Hiding an inherited choice on a child table needs an
66
+ * override row on the child, which this verb does not write. For the x_cadso_* tables
67
+ * this family targets that case does not arise; on extended OOB tables, treat a
68
+ * surprising "missing" as a signal to check the parent.
69
+ */
70
+ export declare function removeChoicesFromField(client: ServiceNowClient, params: RemoveChoicesParams): Promise<RemoveChoicesResult>;
package/dist/choices.js CHANGED
@@ -9,8 +9,59 @@
9
9
  * so choices stay in the same application as the field.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ChoiceWriteError = void 0;
12
13
  exports.encodeQueryValue = encodeQueryValue;
13
14
  exports.addChoicesToField = addChoicesToField;
15
+ exports.removeChoicesFromField = removeChoicesFromField;
16
+ /**
17
+ * Thrown when a choices write fails partway through the value loop.
18
+ *
19
+ * Both verbs write one value at a time. Without this, a throw on value 3 of 5
20
+ * unwound the whole call and took the local `results` array with it — the
21
+ * caller could not tell which values had already been deactivated (or which
22
+ * sys_choice rows had already been created on the add path) without going and
23
+ * querying the instance. That is the worst possible moment to lose the record:
24
+ * the writes already landed and are already captured in the update set.
25
+ *
26
+ * `completed` holds the values that finished before the failure, in order, in
27
+ * exactly the shape they would have had in a successful result. `failedValue`
28
+ * is the value being processed when the write threw, and is NOT in `completed`
29
+ * — its state on the instance is unknown (the row may or may not have been
30
+ * written before the error), so it needs checking by hand.
31
+ */
32
+ class ChoiceWriteError extends Error {
33
+ completed;
34
+ failedValue;
35
+ cause;
36
+ constructor(verb, failedValue, completed, cause) {
37
+ var detail = cause instanceof Error
38
+ ? cause.message
39
+ : String(cause == null ? "" : cause);
40
+ super(verb +
41
+ ' failed on value "' +
42
+ failedValue +
43
+ '" after completing ' +
44
+ completed.length +
45
+ " value(s): " +
46
+ detail +
47
+ "\nAlready written (captured in the update set): " +
48
+ (completed.length === 0
49
+ ? "none"
50
+ : completed
51
+ .map(function (r) {
52
+ return r.value + " [" + r.action + "]";
53
+ })
54
+ .join(", ")) +
55
+ '\nState of "' +
56
+ failedValue +
57
+ '" on the instance is unknown — verify it directly.');
58
+ this.name = "ChoiceWriteError";
59
+ this.completed = completed;
60
+ this.failedValue = failedValue;
61
+ this.cause = cause;
62
+ }
63
+ }
64
+ exports.ChoiceWriteError = ChoiceWriteError;
14
65
  function encodeQueryValue(v) {
15
66
  // ServiceNow encoded-query values: commas/carets/equals are special. We
16
67
  // don't expect them in table/column/value/language inputs, but keep this
@@ -75,8 +126,110 @@ async function fetchUpdateSet(client, sysId) {
75
126
  }
76
127
  return row;
77
128
  }
78
- async function fetchExistingChoices(client, table, column) {
79
- return client.table.query("sys_choice", "name=" + encodeQueryValue(table) + "^element=" + encodeQueryValue(column), 1000);
129
+ var CHOICE_PAGE_LIMIT = 1000;
130
+ /**
131
+ * Values per encoded-query batch. Keeps `valueIN a,b,c…` comfortably inside practical
132
+ * URL limits when a caller passes a long list.
133
+ */
134
+ var CHOICE_QUERY_BATCH = 50;
135
+ /**
136
+ * Read the existing sys_choice rows for JUST the values being acted on.
137
+ *
138
+ * Scoped rather than whole-field on purpose. Reading every row of the field means the
139
+ * result set grows with the field, not with the request, and `client.table.query` has no
140
+ * `sysparm_offset` — so a field with more choices than one page could only be handled by
141
+ * guessing at a truncated read (silently wrong: the add path inserts a duplicate, the
142
+ * remove path reports a live value as "missing") or by refusing outright, which would
143
+ * make both verbs unusable on large choice sets. Asking only for the requested values
144
+ * bounds the read by the request, so large fields stay editable and there is nothing to
145
+ * truncate.
146
+ *
147
+ * Note that values are interpolated into an encoded query, so `encodeQueryValue` rejects
148
+ * ones carrying `,` `^` `=`. Both CLI surfaces already split on those characters, so such
149
+ * values were never expressible there; the library paths now refuse them loudly rather
150
+ * than building a malformed query.
151
+ */
152
+ async function fetchExistingChoices(client, table, column, values) {
153
+ var base = "name=" + encodeQueryValue(table) + "^element=" + encodeQueryValue(column);
154
+ var out = [];
155
+ for (var i = 0; i < values.length; i += CHOICE_QUERY_BATCH) {
156
+ var batch = values.slice(i, i + CHOICE_QUERY_BATCH).map(encodeQueryValue);
157
+ // Ask for one MORE than the ceiling: requesting exactly it cannot distinguish "there
158
+ // are exactly that many" from "there are more and this is page 1".
159
+ var rows = await client.table.query("sys_choice", base + "^valueIN" + batch.join(","), CHOICE_PAGE_LIMIT + 1);
160
+ // A backstop, not an everyday path: this batch asked for at most CHOICE_QUERY_BATCH
161
+ // values, so overflowing a page means one value has an absurd number of duplicate
162
+ // rows. Refuse rather than act on a read that may be incomplete.
163
+ if (rows.length > CHOICE_PAGE_LIMIT) {
164
+ throw new Error("Refusing to act on a truncated choice list: " +
165
+ table +
166
+ "." +
167
+ column +
168
+ " returned more than " +
169
+ CHOICE_PAGE_LIMIT +
170
+ " sys_choice rows for a single batch of requested values, so the read cannot " +
171
+ "be trusted to be complete.");
172
+ }
173
+ out = out.concat(rows);
174
+ }
175
+ return out;
176
+ }
177
+ /**
178
+ * Group the instance's rows by language::value, keeping EVERY row per key.
179
+ *
180
+ * sys_choice has no uniqueness constraint on (name, element, value, language), so a
181
+ * field can genuinely hold two live rows for one value. Indexing to a single row would
182
+ * act on one and silently leave the other — the tool reports success while the choice
183
+ * still renders in the dropdown.
184
+ */
185
+ function groupExistingByKey(rows) {
186
+ // Null-prototype: keys derive from record data, and Object.prototype members must not
187
+ // masquerade as existing entries. See dedupe() for the failure this avoids.
188
+ var byKey = Object.create(null);
189
+ rows.forEach(function (row) {
190
+ var key = (row.language || "en") + "::" + row.value;
191
+ if (!byKey[key])
192
+ byKey[key] = [];
193
+ byKey[key].push(row);
194
+ });
195
+ return byKey;
196
+ }
197
+ /**
198
+ * Collapse choices that target the same language::value. Position is first-seen, but
199
+ * the LAST spec wins, so `[{a,"Old"},{a,"New"}]` upserts the label the caller asked for
200
+ * most recently rather than silently writing both.
201
+ */
202
+ function dedupeChoices(choices) {
203
+ var byKey = Object.create(null);
204
+ var order = [];
205
+ choices.forEach(function (c) {
206
+ var key = (c.language || "en") + "::" + c.value;
207
+ if (!byKey[key])
208
+ order.push(key);
209
+ byKey[key] = c;
210
+ });
211
+ return order.map(function (key) {
212
+ return byKey[key];
213
+ });
214
+ }
215
+ /**
216
+ * Preserve first-seen order while dropping repeats.
217
+ *
218
+ * The lookup is a null-prototype map because the keys are raw choice values: on a plain
219
+ * `{}`, `seen["constructor"]` / `["toString"]` / `["__proto__"]` are already truthy via
220
+ * Object.prototype, so a value with one of those names would be treated as a repeat and
221
+ * silently dropped from the request — never written, never even reported.
222
+ */
223
+ function dedupe(values) {
224
+ var seen = Object.create(null);
225
+ var out = [];
226
+ values.forEach(function (v) {
227
+ if (seen[v])
228
+ return;
229
+ seen[v] = true;
230
+ out.push(v);
231
+ });
232
+ return out;
80
233
  }
81
234
  function buildChoiceFields(table, column, choice, scope) {
82
235
  var fields = {
@@ -134,27 +287,40 @@ async function addChoicesToField(client, params) {
134
287
  });
135
288
  choiceNow = targetChoiceType;
136
289
  }
137
- var existing = await fetchExistingChoices(client, params.table, params.column);
138
- var existingByValue = {};
139
- existing.forEach(function (row) {
140
- var key = (row.language || "en") + "::" + row.value;
141
- existingByValue[key] = row;
142
- });
290
+ // Collapse repeats on language::value, last spec wins. Without this a value listed
291
+ // twice is created twice — the cache is read once up front, so the second pass still
292
+ // sees "not present" and inserts a genuine duplicate sys_choice row.
293
+ var choices = dedupeChoices(params.choices);
294
+ // Read only the values being upserted — see fetchExistingChoices on why the read is
295
+ // scoped to the request rather than the whole field.
296
+ var existing = await fetchExistingChoices(client, params.table, params.column, dedupe(choices.map(function (c) {
297
+ return c.value;
298
+ })));
299
+ var existingByValue = groupExistingByKey(existing);
143
300
  var results = [];
144
- for (var i = 0; i < params.choices.length; i += 1) {
145
- var choice = params.choices[i];
301
+ for (var i = 0; i < choices.length; i += 1) {
302
+ var choice = choices[i];
146
303
  var key = (choice.language || "en") + "::" + choice.value;
147
- var match = existingByValue[key];
148
- if (match && isUnchanged(match, choice)) {
304
+ var matches = existingByValue[key] || [];
305
+ var matchSysIds = matches.map(function (row) {
306
+ return row.sys_id;
307
+ });
308
+ // Every live row for this value, not just one: a field can already hold duplicates,
309
+ // and updating one would leave the other showing the old label in the dropdown.
310
+ var stale = matches.filter(function (row) {
311
+ return !isUnchanged(row, choice);
312
+ });
313
+ if (matches.length > 0 && stale.length === 0) {
149
314
  results.push({
150
315
  value: choice.value,
151
316
  label: choice.label,
152
- sysId: match.sys_id,
317
+ sysId: matchSysIds[0],
318
+ sysIds: matchSysIds,
153
319
  action: "unchanged",
154
320
  });
155
321
  continue;
156
322
  }
157
- if (match) {
323
+ if (matches.length > 0) {
158
324
  var updFields = {
159
325
  label: choice.label,
160
326
  language: choice.language || "en",
@@ -163,30 +329,45 @@ async function addChoicesToField(client, params) {
163
329
  if (choice.sequence != null) {
164
330
  updFields.sequence = String(choice.sequence);
165
331
  }
166
- await client.claude.pushWithUpdateSet({
167
- update_set_sys_id: params.updateSetSysId,
168
- table: "sys_choice",
169
- record_sys_id: match.sys_id,
170
- fields: updFields,
171
- });
332
+ try {
333
+ for (var s = 0; s < stale.length; s += 1) {
334
+ await client.claude.pushWithUpdateSet({
335
+ update_set_sys_id: params.updateSetSysId,
336
+ table: "sys_choice",
337
+ record_sys_id: stale[s].sys_id,
338
+ fields: updFields,
339
+ });
340
+ }
341
+ }
342
+ catch (e) {
343
+ throw new ChoiceWriteError("add-choices", choice.value, results, e);
344
+ }
172
345
  results.push({
173
346
  value: choice.value,
174
347
  label: choice.label,
175
- sysId: match.sys_id,
348
+ sysId: matchSysIds[0],
349
+ sysIds: matchSysIds,
176
350
  action: "updated",
177
351
  });
178
352
  continue;
179
353
  }
180
- var created = await client.claude.createRecord({
181
- table: "sys_choice",
182
- fields: buildChoiceFields(params.table, params.column, choice, dict.sys_scope),
183
- scope: scopeName,
184
- update_set_sys_id: params.updateSetSysId,
185
- });
354
+ var created;
355
+ try {
356
+ created = await client.claude.createRecord({
357
+ table: "sys_choice",
358
+ fields: buildChoiceFields(params.table, params.column, choice, dict.sys_scope),
359
+ scope: scopeName,
360
+ update_set_sys_id: params.updateSetSysId,
361
+ });
362
+ }
363
+ catch (e) {
364
+ throw new ChoiceWriteError("add-choices", choice.value, results, e);
365
+ }
186
366
  results.push({
187
367
  value: choice.value,
188
368
  label: choice.label,
189
369
  sysId: created.sys_id,
370
+ sysIds: [created.sys_id],
190
371
  action: "created",
191
372
  });
192
373
  }
@@ -201,3 +382,107 @@ async function addChoicesToField(client, params) {
201
382
  choices: results,
202
383
  };
203
384
  }
385
+ /**
386
+ * Soft-delete choice values for a field: set `inactive=true` on each matching
387
+ * sys_choice row via pushWithUpdateSet. NEVER a hard delete — the row stays, so the
388
+ * change is reversible and the historical value still resolves on records that
389
+ * already hold it. (A hard drop of sys_choice is deliberately deferred; see DEV-511.)
390
+ *
391
+ * Idempotent:
392
+ * - active value -> "deactivated" (every live row for it flipped to inactive —
393
+ * usually one write, but more when the field holds duplicates)
394
+ * - already inactive -> "unchanged" (no write)
395
+ * - value not found -> "missing" (no write)
396
+ *
397
+ * The dictionary row is fetched only to PROVE the field exists — without that guard a
398
+ * mistyped column reports every value as "missing", the silent-failure this family
399
+ * exists to catch. sys_dictionary.choice is left alone on purpose: removing values
400
+ * does not un-make the column a choice field.
401
+ *
402
+ * Repeated values are collapsed before the loop, so `["a", "a"]` costs no more than
403
+ * `["a"]` and yields one result row — an idempotent verb must not depend on the caller
404
+ * de-duplicating first. (That is "no EXTRA writes from repeats", not "exactly one
405
+ * write": a single value still takes one write per live duplicate row on the field.)
406
+ *
407
+ * LIMITATION — inherited choices. Matching is scoped to `sys_choice.name = <table>`, so
408
+ * a value defined on a PARENT table (task.state inherited by a child) reports "missing"
409
+ * rather than being deactivated. Hiding an inherited choice on a child table needs an
410
+ * override row on the child, which this verb does not write. For the x_cadso_* tables
411
+ * this family targets that case does not arise; on extended OOB tables, treat a
412
+ * surprising "missing" as a signal to check the parent.
413
+ */
414
+ async function removeChoicesFromField(client, params) {
415
+ if (!params.updateSetSysId) {
416
+ throw new Error("updateSetSysId is required — every write must be captured in a named update set.");
417
+ }
418
+ if (!params.values || params.values.length === 0) {
419
+ throw new Error("values must be a non-empty array.");
420
+ }
421
+ var language = params.language || "en";
422
+ // fetchDictionary throws a clear error when the field does not exist; fetchUpdateSet
423
+ // throws unless the set is in progress. Both mirror the add path's guards.
424
+ var dict = await fetchDictionary(client, params.table, params.column);
425
+ var updateSet = await fetchUpdateSet(client, params.updateSetSysId);
426
+ // Collapse repeats: without this a duplicated value writes twice and is counted
427
+ // twice in the summary, which contradicts the idempotency contract.
428
+ var values = dedupe(params.values);
429
+ // Read only the values being removed — see fetchExistingChoices on why the read is
430
+ // scoped to the request rather than the whole field.
431
+ var existing = await fetchExistingChoices(client, params.table, params.column, values);
432
+ var existingByValue = groupExistingByKey(existing);
433
+ var results = [];
434
+ for (var i = 0; i < values.length; i += 1) {
435
+ var value = values[i];
436
+ var matches = existingByValue[language + "::" + value] || [];
437
+ if (matches.length === 0) {
438
+ results.push({ value: value, sysId: "", sysIds: [], action: "missing" });
439
+ continue;
440
+ }
441
+ var sysIds = matches.map(function (row) {
442
+ return row.sys_id;
443
+ });
444
+ // Deactivate EVERY live row for this value. A field can hold more than one, and
445
+ // stopping at the first leaves the choice selectable while reporting success.
446
+ var active = matches.filter(function (row) {
447
+ return row.inactive !== "true";
448
+ });
449
+ if (active.length === 0) {
450
+ results.push({
451
+ value: value,
452
+ sysId: sysIds[0],
453
+ sysIds: sysIds,
454
+ action: "unchanged",
455
+ });
456
+ continue;
457
+ }
458
+ try {
459
+ for (var a = 0; a < active.length; a += 1) {
460
+ await client.claude.pushWithUpdateSet({
461
+ update_set_sys_id: params.updateSetSysId,
462
+ table: "sys_choice",
463
+ record_sys_id: active[a].sys_id,
464
+ fields: { inactive: "true" },
465
+ });
466
+ }
467
+ }
468
+ catch (e) {
469
+ throw new ChoiceWriteError("remove-choices", value, results, e);
470
+ }
471
+ results.push({
472
+ value: value,
473
+ sysId: sysIds[0],
474
+ sysIds: sysIds,
475
+ action: "deactivated",
476
+ });
477
+ }
478
+ return {
479
+ field: {
480
+ table: params.table,
481
+ column: params.column,
482
+ language: language,
483
+ dictionarySysId: dict.sys_id,
484
+ },
485
+ updateSet: { sysId: updateSet.sys_id, name: updateSet.name },
486
+ choices: results,
487
+ };
488
+ }