@tenonhq/dovetail-servicenow 0.0.31 → 0.0.32

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/dist/cli.js CHANGED
@@ -895,6 +895,14 @@ function printHelp() {
895
895
  " Shorten or clear those values first, then re-run.\n" +
896
896
  " --element / --internal-type are REFUSED with an explanation:\n" +
897
897
  " ServiceNow silently ignores both on an existing column.\n" +
898
+ " set-table Update an EXISTING TABLE's own dictionary row (the collection row),\n" +
899
+ " into an update set, then verify against the instance\n" +
900
+ " (--table <t> --update-set <sys_id> [--audit true|false]\n" +
901
+ " [--dry-run] [--json])\n" +
902
+ " --audit turns RECORD AUDITING on/off for the whole table: with it\n" +
903
+ " true ServiceNow writes a sys_audit row per changed field on every\n" +
904
+ " insert and update — a real cost on a high-write table.\n" +
905
+ " Column attributes belong to set-column, record values to set-field.\n" +
898
906
  " invoke-rest Invoke an arbitrary authenticated REST operation (Scripted REST incl.)\n" +
899
907
  " DRY-RUN BY DEFAULT — nothing is sent without --confirm\n" +
900
908
  " (--method <GET|POST|PUT|DELETE> --path /api/<scope>/<service>/<resource>\n" +
@@ -1118,12 +1126,12 @@ async function runAddColumn(flags) {
1118
1126
  }
1119
1127
  /** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
1120
1128
  * false. Anything else is rejected rather than quietly coerced to `true`. */
1121
- function parseBoolFlag(name, raw) {
1129
+ function parseBoolFlag(name, raw, verb = "set-column") {
1122
1130
  if (raw === "true")
1123
1131
  return true;
1124
1132
  if (raw === "false")
1125
1133
  return false;
1126
- throw new Error("set-column: --" + name + " must be true or false (got '" + raw + "').");
1134
+ throw new Error(verb + ": --" + name + " must be true or false (got '" + raw + "').");
1127
1135
  }
1128
1136
  /**
1129
1137
  * dove-sn set-column:
@@ -1221,6 +1229,71 @@ async function runSetColumn(flags, bare) {
1221
1229
  return 2;
1222
1230
  return 0;
1223
1231
  }
1232
+ /**
1233
+ * dove-sn set-table:
1234
+ * --table x_cadso_core_setting --audit true --update-set <sys_id>
1235
+ * [--dry-run] [--json]
1236
+ *
1237
+ * Updates the TABLE's own dictionary row (the `internal_type=collection` row, whose
1238
+ * `element` is empty) — not a column's. Column attributes belong to set-column; a
1239
+ * record's values belong to set-field.
1240
+ */
1241
+ async function runSetTable(flags, bare) {
1242
+ // Guard the string flags AND the updateSetSysId alias: a value-less string flag
1243
+ // arrives as the literal "true", so --update-set (or its alias) with nothing after
1244
+ // it would silently become the sys_id "true" and later fail as "not found".
1245
+ var stringFlags = ["table", "update-set", "updateSetSysId"];
1246
+ for (var f = 0; f < stringFlags.length; f += 1) {
1247
+ if (bare[stringFlags[f]]) {
1248
+ process.stderr.write("set-table: --" + stringFlags[f] + " needs a value (it was given none).\n");
1249
+ return 1;
1250
+ }
1251
+ }
1252
+ var table = flags.table;
1253
+ if (!table) {
1254
+ process.stderr.write("set-table: --table is required " +
1255
+ "(--update-set is required too, unless --dry-run)\n");
1256
+ return 1;
1257
+ }
1258
+ var attributes = {};
1259
+ if (flags.audit !== undefined) {
1260
+ attributes.audit = parseBoolFlag("audit", flags.audit, "set-table");
1261
+ }
1262
+ var params = {
1263
+ client: (0, client_1.createClient)({}),
1264
+ table: table,
1265
+ attributes: attributes,
1266
+ };
1267
+ var setTableUs = flags["update-set"] || flags.updateSetSysId;
1268
+ if (setTableUs)
1269
+ params.updateSetSysId = setTableUs;
1270
+ if (flags["dry-run"] === "true")
1271
+ params.dryRun = true;
1272
+ var result = await (0, table_1.setTable)(params);
1273
+ if (flags.json === "true") {
1274
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1275
+ }
1276
+ else {
1277
+ process.stdout.write("[" +
1278
+ result.status +
1279
+ "] " +
1280
+ result.table +
1281
+ (result.verified && result.status === "applied" ? " — verified" : "") +
1282
+ (result.status === "applied" && !result.capturedInUpdateSet
1283
+ ? " — NOT CAPTURED"
1284
+ : "") +
1285
+ "\n" +
1286
+ result.note +
1287
+ "\n");
1288
+ }
1289
+ // 2 = the write landed but the instance does not reflect it (or it was not
1290
+ // captured), which must not read as success to a script.
1291
+ if (result.status === "failed")
1292
+ return 2;
1293
+ if (result.status === "applied" && !result.capturedInUpdateSet)
1294
+ return 2;
1295
+ return 0;
1296
+ }
1224
1297
  /** Parse inline `--fields "k=v, k2=v2"` into a field map. */
1225
1298
  function parseFieldsInline(input) {
1226
1299
  var out = {};
@@ -1659,6 +1732,9 @@ async function main() {
1659
1732
  if (parsed.command === "set-column") {
1660
1733
  return await runSetColumn(parsed.flags, parsed.bare);
1661
1734
  }
1735
+ if (parsed.command === "set-table") {
1736
+ return await runSetTable(parsed.flags, parsed.bare);
1737
+ }
1662
1738
  if (parsed.command === "set-field") {
1663
1739
  return await runSetField(parsed.flags);
1664
1740
  }
package/dist/index.d.ts CHANGED
@@ -19,8 +19,8 @@ export { sincPlugin } from "./plugin";
19
19
  export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, triggerPublication, publishActionType, editActionType, applyStepOps, verifySteps, summarizeSteps, formatStepPill, readFlow, readActionType, publishFlow, copyFlow, createFlow, buildPublishModel, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError, } from "./flowDesigner";
20
20
  export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, EditActionTypeParams, EditActionTypeResult, EditActionTypeOps, StepOps, StepRecord, StepSummary, StepIoSummary, PatchStepScriptOp, AddStepOutputOp, AddStepInputOp, ApplyStepOpsResult, VerifyStepsResult, ReadFlowParams, ReadFlowResult, FlowStep, FlowVariable, ReadActionTypeParams, ReadActionTypeResult, ActionIo, PublishFlowParams, PublishFlowResult, CopyFlowParams, CopyFlowResult, CreateFlowParams, CreateFlowResult, EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch, TestFlowParams, TestFlowResult, WriteOp, WriteOpResult, } from "./flowDesigner";
21
21
  export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult, } from "./types";
22
- export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, setColumn, resolveAttributes, toStoredValue, } from "./table";
23
- export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
22
+ export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
23
+ export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
24
24
  export { setField } from "./setField";
25
25
  export { createRecord } from "./createRecord";
26
26
  export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setField";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.formatStepPill = exports.summarizeSteps = exports.verifySteps = exports.applyStepOps = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
10
- exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = void 0;
10
+ exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = void 0;
11
11
  var client_1 = require("./client");
12
12
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
13
13
  var createClientFromEnvFile_1 = require("./createClientFromEnvFile");
@@ -75,6 +75,8 @@ Object.defineProperty(exports, "deriveElement", { enumerable: true, get: functio
75
75
  Object.defineProperty(exports, "setColumn", { enumerable: true, get: function () { return table_1.setColumn; } });
76
76
  Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return table_1.resolveAttributes; } });
77
77
  Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return table_1.toStoredValue; } });
78
+ Object.defineProperty(exports, "setTable", { enumerable: true, get: function () { return table_1.setTable; } });
79
+ Object.defineProperty(exports, "resolveTableAttributes", { enumerable: true, get: function () { return table_1.resolveTableAttributes; } });
78
80
  var setField_1 = require("./setField");
79
81
  Object.defineProperty(exports, "setField", { enumerable: true, get: function () { return setField_1.setField; } });
80
82
  var createRecord_1 = require("./createRecord");
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
12
12
  import type { ServiceNowClient } from "../client";
13
- export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "action_edit", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_column", "set_field", "create_record", "host_assets", "invoke_rest", "app_publish"];
13
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "action_edit", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_column", "set_table", "set_field", "create_record", "host_assets", "invoke_rest", "app_publish"];
14
14
  export type ToolName = (typeof TOOL_NAMES)[number];
15
15
  export interface RegistryDeps {
16
16
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -50,6 +50,7 @@ exports.TOOL_NAMES = [
50
50
  "create_table",
51
51
  "add_column",
52
52
  "set_column",
53
+ "set_table",
53
54
  "set_field",
54
55
  "create_record",
55
56
  "host_assets",
@@ -393,6 +394,42 @@ function buildDescriptors(deps = {}) {
393
394
  });
394
395
  },
395
396
  },
397
+ {
398
+ name: "set_table",
399
+ annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
400
+ description: "Update an EXISTING ServiceNow TABLE's own dictionary attributes — the row where " +
401
+ "internal_type=collection and element is EMPTY — captured into a named update set, then " +
402
+ "READ BACK from the instance to verify. Completes the trio: set_field changes a RECORD's " +
403
+ "value, set_column changes a COLUMN's definition, set_table changes the TABLE's own. " +
404
+ "Currently settable: audit. audit=true turns RECORD AUDITING on for the whole table — " +
405
+ "ServiceNow then writes a sys_audit row per changed field on every insert and update, " +
406
+ "which is a real storage and write cost on a high-volume table, so weigh it before " +
407
+ "enabling. Attributes are a closed set, never an open field map; a COLUMN attribute " +
408
+ "(label/mandatory/default/maxLength/readOnly/element/internalType) is refused by name and " +
409
+ "redirected to set_column. When the requested value already matches, nothing is written " +
410
+ "and the status is 'unchanged' — note that an identical-value write also captures " +
411
+ "NOTHING, so there is no update-set row to promote. dryRun:true diffs against the " +
412
+ "instance and writes nothing.",
413
+ shape: schemas_1.setTableSchema.shape,
414
+ handler: async function (args) {
415
+ var p = schemas_1.setTableSchema.parse(args);
416
+ // Same live-path guard as set_column: the schema leaves updateSetSysId
417
+ // optional so dry-run works without one.
418
+ if (p.dryRun !== true &&
419
+ (!p.updateSetSysId || !p.updateSetSysId.trim())) {
420
+ throw new Error("set_table: updateSetSysId is required on the live path so the schema " +
421
+ "change is captured in a known update set — set dryRun:true to plan " +
422
+ "without one.");
423
+ }
424
+ return (0, table_1.setTable)({
425
+ client: client(),
426
+ table: p.table,
427
+ attributes: p.attributes,
428
+ updateSetSysId: p.updateSetSysId,
429
+ dryRun: p.dryRun,
430
+ });
431
+ },
432
+ },
396
433
  {
397
434
  name: "set_field",
398
435
  annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
@@ -914,6 +914,43 @@ export declare var setColumnSchema: z.ZodObject<{
914
914
  dryRun?: boolean | undefined;
915
915
  updateSetSysId?: string | undefined;
916
916
  }>;
917
+ export declare var tableAttributesSchema: z.ZodObject<{
918
+ audit: z.ZodOptional<z.ZodBoolean>;
919
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
920
+ audit: z.ZodOptional<z.ZodBoolean>;
921
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
922
+ audit: z.ZodOptional<z.ZodBoolean>;
923
+ }, z.ZodTypeAny, "passthrough">>;
924
+ export declare var setTableSchema: z.ZodObject<{
925
+ table: z.ZodString;
926
+ attributes: z.ZodObject<{
927
+ audit: z.ZodOptional<z.ZodBoolean>;
928
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
929
+ audit: z.ZodOptional<z.ZodBoolean>;
930
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
931
+ audit: z.ZodOptional<z.ZodBoolean>;
932
+ }, z.ZodTypeAny, "passthrough">>;
933
+ updateSetSysId: z.ZodOptional<z.ZodString>;
934
+ dryRun: z.ZodOptional<z.ZodBoolean>;
935
+ }, "strip", z.ZodTypeAny, {
936
+ table: string;
937
+ attributes: {
938
+ audit?: boolean | undefined;
939
+ } & {
940
+ [k: string]: unknown;
941
+ };
942
+ dryRun?: boolean | undefined;
943
+ updateSetSysId?: string | undefined;
944
+ }, {
945
+ table: string;
946
+ attributes: {
947
+ audit?: boolean | undefined;
948
+ } & {
949
+ [k: string]: unknown;
950
+ };
951
+ dryRun?: boolean | undefined;
952
+ updateSetSysId?: string | undefined;
953
+ }>;
917
954
  export declare var setFieldSchema: z.ZodObject<{
918
955
  table: z.ZodString;
919
956
  sysId: z.ZodOptional<z.ZodString>;
@@ -4,7 +4,7 @@
4
4
  * own file so registry.ts stays focused on wiring.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.invokeRestSchema = exports.publishAppSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
7
+ exports.invokeRestSchema = exports.publishAppSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setTableSchema = exports.tableAttributesSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
8
8
  const zod_1 = require("zod");
9
9
  exports.createViewSchema = zod_1.z.object({
10
10
  name: zod_1.z.string().min(1),
@@ -241,6 +241,28 @@ exports.setColumnSchema = zod_1.z.object({
241
241
  updateSetSysId: zod_1.z.string().min(1).optional(),
242
242
  dryRun: zod_1.z.boolean().optional(),
243
243
  });
244
+ // set-table takes a CLOSED attribute set for the same reason set-column does: an
245
+ // unbounded write to sys_dictionary lets a caller silently corrupt schema. These are
246
+ // the TABLE's own attributes (the internal_type=collection row), not a column's.
247
+ //
248
+ // .passthrough() is deliberate: z.object() strips unknown keys by default, which would
249
+ // silently drop a column attribute (or a typo) BEFORE resolveTableAttributes could see
250
+ // it — so { audit:true, label:"x" } would succeed, quietly discarding label. Passing
251
+ // unknown keys through lets the core allowlist reject them (a column attribute earns the
252
+ // "use set-column" redirect; anything else, "not a settable table attribute").
253
+ exports.tableAttributesSchema = zod_1.z
254
+ .object({
255
+ audit: zod_1.z.boolean().optional(),
256
+ })
257
+ .passthrough();
258
+ exports.setTableSchema = zod_1.z.object({
259
+ table: zod_1.z.string().min(1),
260
+ attributes: exports.tableAttributesSchema,
261
+ // Optional because dryRun works without one; the live-path requirement is
262
+ // enforced at the tool boundary (registry.ts), matching set_column.
263
+ updateSetSysId: zod_1.z.string().min(1).optional(),
264
+ dryRun: zod_1.z.boolean().optional(),
265
+ });
244
266
  // Data-record write verbs. Kept as plain z.object (no .refine wrapper) so
245
267
  // registry.ts can read `.shape`; the deeper rules — one of sysId/query, at
246
268
  // least one field, the schema-table refusal — are enforced by the core
@@ -15,3 +15,5 @@ export { addColumn, deriveElement } from "./addColumn";
15
15
  export type { AddColumnParams, AddColumnResult } from "./addColumn";
16
16
  export { setColumn, resolveAttributes, toStoredValue, findTruncationRisk, } from "./setColumn";
17
17
  export type { SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, TruncationRisk, } from "./setColumn";
18
+ export { setTable, resolveTableAttributes } from "./setTable";
19
+ export type { SetTableParams, SetTableResult, TableAttributes, } from "./setTable";
@@ -5,7 +5,7 @@
5
5
  * live-validation caveat.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
8
+ exports.resolveTableAttributes = exports.setTable = exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
9
9
  var createTable_1 = require("./createTable");
10
10
  Object.defineProperty(exports, "createTable", { enumerable: true, get: function () { return createTable_1.createTable; } });
11
11
  Object.defineProperty(exports, "projectTableGraph", { enumerable: true, get: function () { return createTable_1.projectTableGraph; } });
@@ -42,3 +42,6 @@ Object.defineProperty(exports, "setColumn", { enumerable: true, get: function ()
42
42
  Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return setColumn_1.resolveAttributes; } });
43
43
  Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return setColumn_1.toStoredValue; } });
44
44
  Object.defineProperty(exports, "findTruncationRisk", { enumerable: true, get: function () { return setColumn_1.findTruncationRisk; } });
45
+ var setTable_1 = require("./setTable");
46
+ Object.defineProperty(exports, "setTable", { enumerable: true, get: function () { return setTable_1.setTable; } });
47
+ Object.defineProperty(exports, "resolveTableAttributes", { enumerable: true, get: function () { return setTable_1.resolveTableAttributes; } });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * dove-sn set-table — update the dictionary attributes of an EXISTING ServiceNow
3
+ * TABLE, captured into a named update set, then read back and verified against the
4
+ * instance.
5
+ *
6
+ * WHY THIS IS NOT set-column. Every table carries one sys_dictionary row that
7
+ * describes the TABLE rather than any column: `internal_type=collection`, with
8
+ * `element` empty. Table-wide settings live there. `set-column` targets
9
+ * `name=<table>^element=<column>` and its whole apparatus — inheritance walking,
10
+ * the max_length ALTER, the shrink guard — is column-shaped; none of it applies to
11
+ * a collection row. And `set-field` is a DATA verb that refuses sys_dictionary
12
+ * outright. So the collection row had no owner. This is it.
13
+ *
14
+ * The split that settled in practice, extended one step:
15
+ * "field" = a record's value -> set-field
16
+ * "column" = a table's column schema -> set-column
17
+ * "table" = the table's own schema -> set-table (this verb)
18
+ *
19
+ * AUDIT IS A TABLE-LEVEL FLAG. `sys_dictionary.audit` on the collection row is what
20
+ * turns record auditing on for a table: with it true, ServiceNow writes a sys_audit
21
+ * row per changed field on every insert and update. That is a genuine cost on a
22
+ * high-write table, so this verb reports the before -> after honestly and never
23
+ * writes a value over itself.
24
+ *
25
+ * THE ALLOWLIST IS DELIBERATELY SMALL. An unbounded write to sys_dictionary lets a
26
+ * caller silently corrupt schema, so — exactly as set-column does — only named
27
+ * attributes are settable and everything else is refused by name. Widen WRITABLE
28
+ * only for an attribute whose behaviour on a collection row has been verified live.
29
+ */
30
+ import type { ServiceNowClient } from "../client";
31
+ import type { AttributeChange } from "./setColumn";
32
+ export interface TableAttributes {
33
+ /**
34
+ * Record auditing for the whole table (sys_dictionary.audit on the collection
35
+ * row). True makes ServiceNow write a sys_audit row per changed field on every
36
+ * insert and update.
37
+ */
38
+ audit?: boolean;
39
+ }
40
+ export interface SetTableParams {
41
+ client: ServiceNowClient;
42
+ /** The table, by name (e.g. "x_cadso_core_setting"). */
43
+ table: string;
44
+ /** The attributes to set. At least one is required. */
45
+ attributes: TableAttributes;
46
+ /** Update set to capture the change into. REQUIRED on the live path. */
47
+ updateSetSysId?: string;
48
+ /** Plan only — resolves and diffs, writes nothing. */
49
+ dryRun?: boolean;
50
+ }
51
+ export interface SetTableResult {
52
+ status: "dry-run" | "applied" | "unchanged" | "failed";
53
+ table: string;
54
+ /** sys_id of the table's collection sys_dictionary row. */
55
+ tableSysId: string;
56
+ updateSetSysId: string;
57
+ /** The attributes that differed and were written (empty when nothing changed). */
58
+ changes: Array<AttributeChange>;
59
+ /** True only when every requested value was READ BACK from the instance and matched. */
60
+ verified: boolean;
61
+ /** True when the sys_update_xml row for this table was found in the named update
62
+ * set. A write that lands but is not captured cannot be promoted — it dies on this
63
+ * instance, so a 200 alone is not success. */
64
+ capturedInUpdateSet: boolean;
65
+ note: string;
66
+ }
67
+ /**
68
+ * Translate the caller's friendly attributes into the sys_dictionary columns to
69
+ * write. Throws on anything not on the allowlist, and redirects column attributes
70
+ * to set-column by name rather than reporting them as merely unknown.
71
+ */
72
+ export declare function resolveTableAttributes(attributes: TableAttributes): Record<string, string>;
73
+ export declare function setTable(params: SetTableParams): Promise<SetTableResult>;
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ /**
3
+ * dove-sn set-table — update the dictionary attributes of an EXISTING ServiceNow
4
+ * TABLE, captured into a named update set, then read back and verified against the
5
+ * instance.
6
+ *
7
+ * WHY THIS IS NOT set-column. Every table carries one sys_dictionary row that
8
+ * describes the TABLE rather than any column: `internal_type=collection`, with
9
+ * `element` empty. Table-wide settings live there. `set-column` targets
10
+ * `name=<table>^element=<column>` and its whole apparatus — inheritance walking,
11
+ * the max_length ALTER, the shrink guard — is column-shaped; none of it applies to
12
+ * a collection row. And `set-field` is a DATA verb that refuses sys_dictionary
13
+ * outright. So the collection row had no owner. This is it.
14
+ *
15
+ * The split that settled in practice, extended one step:
16
+ * "field" = a record's value -> set-field
17
+ * "column" = a table's column schema -> set-column
18
+ * "table" = the table's own schema -> set-table (this verb)
19
+ *
20
+ * AUDIT IS A TABLE-LEVEL FLAG. `sys_dictionary.audit` on the collection row is what
21
+ * turns record auditing on for a table: with it true, ServiceNow writes a sys_audit
22
+ * row per changed field on every insert and update. That is a genuine cost on a
23
+ * high-write table, so this verb reports the before -> after honestly and never
24
+ * writes a value over itself.
25
+ *
26
+ * THE ALLOWLIST IS DELIBERATELY SMALL. An unbounded write to sys_dictionary lets a
27
+ * caller silently corrupt schema, so — exactly as set-column does — only named
28
+ * attributes are settable and everything else is refused by name. Widen WRITABLE
29
+ * only for an attribute whose behaviour on a collection row has been verified live.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.resolveTableAttributes = resolveTableAttributes;
33
+ exports.setTable = setTable;
34
+ const setField_1 = require("../setField");
35
+ const choices_1 = require("../choices");
36
+ const setColumn_1 = require("./setColumn");
37
+ /** The table-level dictionary attributes set-table will write, keyed by the friendly
38
+ * name a caller uses. Everything absent from this map is refused. */
39
+ var WRITABLE = {
40
+ audit: "audit",
41
+ };
42
+ /**
43
+ * Attributes that belong to a COLUMN, not the table. Refused by name so that asking
44
+ * for one earns a redirect to set-column instead of a confusing "not settable".
45
+ */
46
+ var COLUMN_ATTRIBUTES = [
47
+ "label",
48
+ "mandatory",
49
+ "default",
50
+ "readOnly",
51
+ "maxLength",
52
+ "internalType",
53
+ "element",
54
+ ];
55
+ /**
56
+ * Translate the caller's friendly attributes into the sys_dictionary columns to
57
+ * write. Throws on anything not on the allowlist, and redirects column attributes
58
+ * to set-column by name rather than reporting them as merely unknown.
59
+ */
60
+ function resolveTableAttributes(attributes) {
61
+ if (!attributes || typeof attributes !== "object") {
62
+ throw new Error("set-table: attributes object is required.");
63
+ }
64
+ var out = {};
65
+ var keys = Object.keys(attributes);
66
+ for (var i = 0; i < keys.length; i += 1) {
67
+ var key = keys[i];
68
+ var value = attributes[key];
69
+ if (value === undefined || value === null)
70
+ continue;
71
+ if (COLUMN_ATTRIBUTES.indexOf(key) !== -1) {
72
+ throw new Error("set-table: '" +
73
+ key +
74
+ "' is a COLUMN attribute, not a table one — it lives on a column's " +
75
+ "dictionary row, not the table's collection row. Use set-column.");
76
+ }
77
+ var target = WRITABLE[key];
78
+ if (!target) {
79
+ throw new Error("set-table: '" +
80
+ key +
81
+ "' is not a settable table attribute. Settable: " +
82
+ Object.keys(WRITABLE).join(", ") +
83
+ ".");
84
+ }
85
+ out[target] = (0, setColumn_1.toStoredValue)(value);
86
+ }
87
+ if (Object.keys(out).length === 0) {
88
+ throw new Error("set-table: at least one attribute is required.");
89
+ }
90
+ return out;
91
+ }
92
+ /**
93
+ * Fetch the table's collection dictionary row — `element` empty, which is what
94
+ * distinguishes the table's own row from every column row sharing its `name`.
95
+ */
96
+ async function fetchCollectionRow(client, table, fields) {
97
+ var rows = await client.table.query("sys_dictionary", "name=" + (0, choices_1.encodeQueryValue)(table) + "^elementISEMPTY", { limit: 1, fields: fields });
98
+ if (rows.length > 0)
99
+ return rows[0];
100
+ throw new Error("set-table: no collection dictionary row found for '" +
101
+ table +
102
+ "'. Check the table name — every real table has exactly one row with " +
103
+ "internal_type=collection and an empty element.");
104
+ }
105
+ /**
106
+ * Require the update set to be open. A write into a Complete/Ignored set is accepted
107
+ * and then silently lost — the change lands on the instance and is captured nowhere,
108
+ * so it can never be promoted.
109
+ */
110
+ async function assertUpdateSetOpen(client, updateSetSysId) {
111
+ var rows = await client.table.query("sys_update_set", "sys_id=" + (0, choices_1.encodeQueryValue)(updateSetSysId), { limit: 1, fields: ["sys_id", "name", "state"] });
112
+ if (rows.length === 0) {
113
+ throw new Error("set-table: update set " + updateSetSysId + " not found on this instance.");
114
+ }
115
+ var state = (0, setField_1.fieldToString)(rows[0].state);
116
+ if (state !== "in progress" && state !== "in_progress") {
117
+ throw new Error("set-table: update set '" +
118
+ (0, setField_1.fieldToString)(rows[0].name) +
119
+ "' is '" +
120
+ state +
121
+ "', not 'in progress'. A change written into a closed set is captured " +
122
+ "nowhere and can never be promoted.");
123
+ }
124
+ }
125
+ /**
126
+ * Assert the change was captured. The collection row's update name is deterministic
127
+ * and ends in `_null` — the element is empty, so ServiceNow names the row for "no
128
+ * column" rather than for a column.
129
+ */
130
+ async function assertCaptured(client, table, updateSetSysId) {
131
+ var name = "sys_dictionary_" + table + "_null";
132
+ var rows = await client.table.query("sys_update_xml", "update_set=" +
133
+ (0, choices_1.encodeQueryValue)(updateSetSysId) +
134
+ "^name=" +
135
+ (0, choices_1.encodeQueryValue)(name), { limit: 1, fields: ["sys_id", "name"] });
136
+ return rows.length > 0;
137
+ }
138
+ function describeChanges(changes) {
139
+ return changes
140
+ .map(function (c) {
141
+ return c.attribute + " " + (c.from === "" ? "(empty)" : c.from) + " -> " + c.to;
142
+ })
143
+ .join(", ");
144
+ }
145
+ async function setTable(params) {
146
+ if (!params || !params.client) {
147
+ throw new Error("set-table: client is required.");
148
+ }
149
+ if (!params.table || !String(params.table).trim()) {
150
+ throw new Error("set-table: --table is required.");
151
+ }
152
+ // The allowlist is enforced BEFORE anything touches the instance, so an
153
+ // unsettable attribute fails on the dry-run too.
154
+ var writes = resolveTableAttributes(params.attributes);
155
+ var targets = Object.keys(writes);
156
+ var table = String(params.table).trim();
157
+ var readFields = ["sys_id", "name", "element", "internal_type"].concat(targets);
158
+ var row = await fetchCollectionRow(params.client, table, readFields);
159
+ var tableSysId = (0, setField_1.fieldToString)(row.sys_id);
160
+ // Diff against what the instance actually stores, so a same-value request is
161
+ // reported as unchanged rather than written over itself and called applied.
162
+ var changes = [];
163
+ for (var i = 0; i < targets.length; i += 1) {
164
+ var field = targets[i];
165
+ var from = (0, setField_1.fieldToString)(row[field]);
166
+ var to = writes[field];
167
+ if (from !== to) {
168
+ changes.push({ attribute: field, from: from, to: to });
169
+ }
170
+ }
171
+ if (params.dryRun) {
172
+ return {
173
+ status: "dry-run",
174
+ table: table,
175
+ tableSysId: tableSysId,
176
+ updateSetSysId: params.updateSetSysId ? String(params.updateSetSysId).trim() : "",
177
+ changes: changes,
178
+ verified: false,
179
+ capturedInUpdateSet: false,
180
+ note: changes.length === 0
181
+ ? "dry-run: no write. " +
182
+ table +
183
+ " already holds every requested value — nothing would change."
184
+ : "dry-run: no write. Would set " +
185
+ describeChanges(changes) +
186
+ " on " +
187
+ table +
188
+ ", captured into update set " +
189
+ (params.updateSetSysId ? params.updateSetSysId : "(none provided)") +
190
+ ".",
191
+ };
192
+ }
193
+ if (!params.updateSetSysId || !String(params.updateSetSysId).trim()) {
194
+ throw new Error("set-table: --update-set <sys_id> is required so the schema change is " +
195
+ "captured and can be promoted. An uncaptured change exists only on this " +
196
+ "instance.");
197
+ }
198
+ var updateSetSysId = String(params.updateSetSysId).trim();
199
+ await assertUpdateSetOpen(params.client, updateSetSysId);
200
+ if (changes.length === 0) {
201
+ return {
202
+ status: "unchanged",
203
+ table: table,
204
+ tableSysId: tableSysId,
205
+ updateSetSysId: updateSetSysId,
206
+ changes: [],
207
+ verified: true,
208
+ capturedInUpdateSet: false,
209
+ note: table +
210
+ " already holds every requested value — nothing written, and nothing " +
211
+ "captured: an identical-value write produces no sys_update_xml row.",
212
+ };
213
+ }
214
+ var fieldsToWrite = {};
215
+ for (var k = 0; k < changes.length; k += 1) {
216
+ fieldsToWrite[changes[k].attribute] = changes[k].to;
217
+ }
218
+ try {
219
+ await params.client.claude.pushWithUpdateSet({
220
+ update_set_sys_id: updateSetSysId,
221
+ table: "sys_dictionary",
222
+ record_sys_id: tableSysId,
223
+ fields: fieldsToWrite,
224
+ });
225
+ }
226
+ catch (e) {
227
+ // The write may or may not have landed — the transport failed, not necessarily
228
+ // the change. Say exactly that rather than leaving the caller to guess.
229
+ return {
230
+ status: "failed",
231
+ table: table,
232
+ tableSysId: tableSysId,
233
+ updateSetSysId: updateSetSysId,
234
+ changes: changes,
235
+ verified: false,
236
+ capturedInUpdateSet: false,
237
+ note: "the write to " +
238
+ table +
239
+ " failed in transport: " +
240
+ (e instanceof Error ? e.message : String(e)) +
241
+ ". Re-read the dictionary row before retrying — the change may or may not " +
242
+ "have landed.",
243
+ };
244
+ }
245
+ // Read back from the instance. A 200 is not proof the value stuck.
246
+ var afterRow = await fetchCollectionRow(params.client, table, readFields);
247
+ var verified = true;
248
+ for (var m = 0; m < targets.length; m += 1) {
249
+ if ((0, setField_1.fieldToString)(afterRow[targets[m]]) !== writes[targets[m]]) {
250
+ verified = false;
251
+ }
252
+ }
253
+ var captured = await assertCaptured(params.client, table, updateSetSysId);
254
+ if (!verified) {
255
+ return {
256
+ status: "failed",
257
+ table: table,
258
+ tableSysId: tableSysId,
259
+ updateSetSysId: updateSetSysId,
260
+ changes: changes,
261
+ verified: false,
262
+ capturedInUpdateSet: captured,
263
+ note: "the write to " +
264
+ table +
265
+ " landed but the read-back does not match what was requested — check " +
266
+ "dictionary ACLs on this instance.",
267
+ };
268
+ }
269
+ return {
270
+ status: "applied",
271
+ table: table,
272
+ tableSysId: tableSysId,
273
+ updateSetSysId: updateSetSysId,
274
+ changes: changes,
275
+ verified: true,
276
+ capturedInUpdateSet: captured,
277
+ note: captured
278
+ ? "Set " +
279
+ describeChanges(changes) +
280
+ " on " +
281
+ table +
282
+ ", verified by read-back and captured in update set " +
283
+ updateSetSysId +
284
+ "."
285
+ : "Set " +
286
+ describeChanges(changes) +
287
+ " on " +
288
+ table +
289
+ " and verified by read-back, but NO sys_update_xml row was found in " +
290
+ "update set " +
291
+ updateSetSysId +
292
+ " — the change cannot be promoted off this instance until it is captured.",
293
+ };
294
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },