@tenonhq/dovetail-servicenow 0.0.31 → 0.0.33
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 +142 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/invokeRest.d.ts +12 -0
- package/dist/invokeRest.js +67 -0
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +37 -0
- package/dist/mcp/schemas.d.ts +37 -0
- package/dist/mcp/schemas.js +23 -1
- package/dist/table/index.d.ts +2 -0
- package/dist/table/index.js +4 -1
- package/dist/table/setTable.d.ts +73 -0
- package/dist/table/setTable.js +294 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -895,10 +895,19 @@ 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" +
|
|
901
|
-
" [--body '<json>' | --body-json <path>] [--confirm] [--dry-run] [--json]
|
|
909
|
+
" [--body '<json>' | --body-json <path>] [--confirm] [--dry-run] [--json]\n" +
|
|
910
|
+
" [--out <file> full JSON result to a file (atomic; overwrites); for large bodies])\n" +
|
|
902
911
|
" set-field Set field value(s) on an EXISTING record, into an update set, then verify\n" +
|
|
903
912
|
" (--table <t> --sys-id <id>|--query <q> --update-set <sys_id>\n" +
|
|
904
913
|
' (--fields "k=v,k2=v2" | --from-json <path>) [--dry-run] [--json])\n' +
|
|
@@ -1118,12 +1127,12 @@ async function runAddColumn(flags) {
|
|
|
1118
1127
|
}
|
|
1119
1128
|
/** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
|
|
1120
1129
|
* false. Anything else is rejected rather than quietly coerced to `true`. */
|
|
1121
|
-
function parseBoolFlag(name, raw) {
|
|
1130
|
+
function parseBoolFlag(name, raw, verb = "set-column") {
|
|
1122
1131
|
if (raw === "true")
|
|
1123
1132
|
return true;
|
|
1124
1133
|
if (raw === "false")
|
|
1125
1134
|
return false;
|
|
1126
|
-
throw new Error("
|
|
1135
|
+
throw new Error(verb + ": --" + name + " must be true or false (got '" + raw + "').");
|
|
1127
1136
|
}
|
|
1128
1137
|
/**
|
|
1129
1138
|
* dove-sn set-column:
|
|
@@ -1221,6 +1230,71 @@ async function runSetColumn(flags, bare) {
|
|
|
1221
1230
|
return 2;
|
|
1222
1231
|
return 0;
|
|
1223
1232
|
}
|
|
1233
|
+
/**
|
|
1234
|
+
* dove-sn set-table:
|
|
1235
|
+
* --table x_cadso_core_setting --audit true --update-set <sys_id>
|
|
1236
|
+
* [--dry-run] [--json]
|
|
1237
|
+
*
|
|
1238
|
+
* Updates the TABLE's own dictionary row (the `internal_type=collection` row, whose
|
|
1239
|
+
* `element` is empty) — not a column's. Column attributes belong to set-column; a
|
|
1240
|
+
* record's values belong to set-field.
|
|
1241
|
+
*/
|
|
1242
|
+
async function runSetTable(flags, bare) {
|
|
1243
|
+
// Guard the string flags AND the updateSetSysId alias: a value-less string flag
|
|
1244
|
+
// arrives as the literal "true", so --update-set (or its alias) with nothing after
|
|
1245
|
+
// it would silently become the sys_id "true" and later fail as "not found".
|
|
1246
|
+
var stringFlags = ["table", "update-set", "updateSetSysId"];
|
|
1247
|
+
for (var f = 0; f < stringFlags.length; f += 1) {
|
|
1248
|
+
if (bare[stringFlags[f]]) {
|
|
1249
|
+
process.stderr.write("set-table: --" + stringFlags[f] + " needs a value (it was given none).\n");
|
|
1250
|
+
return 1;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
var table = flags.table;
|
|
1254
|
+
if (!table) {
|
|
1255
|
+
process.stderr.write("set-table: --table is required " +
|
|
1256
|
+
"(--update-set is required too, unless --dry-run)\n");
|
|
1257
|
+
return 1;
|
|
1258
|
+
}
|
|
1259
|
+
var attributes = {};
|
|
1260
|
+
if (flags.audit !== undefined) {
|
|
1261
|
+
attributes.audit = parseBoolFlag("audit", flags.audit, "set-table");
|
|
1262
|
+
}
|
|
1263
|
+
var params = {
|
|
1264
|
+
client: (0, client_1.createClient)({}),
|
|
1265
|
+
table: table,
|
|
1266
|
+
attributes: attributes,
|
|
1267
|
+
};
|
|
1268
|
+
var setTableUs = flags["update-set"] || flags.updateSetSysId;
|
|
1269
|
+
if (setTableUs)
|
|
1270
|
+
params.updateSetSysId = setTableUs;
|
|
1271
|
+
if (flags["dry-run"] === "true")
|
|
1272
|
+
params.dryRun = true;
|
|
1273
|
+
var result = await (0, table_1.setTable)(params);
|
|
1274
|
+
if (flags.json === "true") {
|
|
1275
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
process.stdout.write("[" +
|
|
1279
|
+
result.status +
|
|
1280
|
+
"] " +
|
|
1281
|
+
result.table +
|
|
1282
|
+
(result.verified && result.status === "applied" ? " — verified" : "") +
|
|
1283
|
+
(result.status === "applied" && !result.capturedInUpdateSet
|
|
1284
|
+
? " — NOT CAPTURED"
|
|
1285
|
+
: "") +
|
|
1286
|
+
"\n" +
|
|
1287
|
+
result.note +
|
|
1288
|
+
"\n");
|
|
1289
|
+
}
|
|
1290
|
+
// 2 = the write landed but the instance does not reflect it (or it was not
|
|
1291
|
+
// captured), which must not read as success to a script.
|
|
1292
|
+
if (result.status === "failed")
|
|
1293
|
+
return 2;
|
|
1294
|
+
if (result.status === "applied" && !result.capturedInUpdateSet)
|
|
1295
|
+
return 2;
|
|
1296
|
+
return 0;
|
|
1297
|
+
}
|
|
1224
1298
|
/** Parse inline `--fields "k=v, k2=v2"` into a field map. */
|
|
1225
1299
|
function parseFieldsInline(input) {
|
|
1226
1300
|
var out = {};
|
|
@@ -1443,6 +1517,11 @@ async function runHostAssets(flags) {
|
|
|
1443
1517
|
* --confirm Send for real. WITHOUT it the command is a DRY-RUN.
|
|
1444
1518
|
* --dry-run Force a dry-run even with --confirm.
|
|
1445
1519
|
* --json Emit the structured InvokeRestResult.
|
|
1520
|
+
* --out <file> Also write the full structured result to a file
|
|
1521
|
+
* (pretty JSON, atomic temp+rename, OVERWRITES an
|
|
1522
|
+
* existing file; parent dir must exist). The reliable
|
|
1523
|
+
* channel for large response bodies - piped stdout is
|
|
1524
|
+
* flush-guarded but a file needs no downstream reader.
|
|
1446
1525
|
*
|
|
1447
1526
|
* Invoke an arbitrary authenticated REST operation (Scripted REST included).
|
|
1448
1527
|
* Dry-run by default; --confirm sends and returns { httpStatus, ok, body } with
|
|
@@ -1489,6 +1568,17 @@ async function runInvokeRest(flags) {
|
|
|
1489
1568
|
params.body = body;
|
|
1490
1569
|
}
|
|
1491
1570
|
var result = await (0, invokeRest_1.invokeRest)(params);
|
|
1571
|
+
if (flags.out) {
|
|
1572
|
+
try {
|
|
1573
|
+
(0, invokeRest_1.writeInvokeRestResultFile)(flags.out, result);
|
|
1574
|
+
}
|
|
1575
|
+
catch (err) {
|
|
1576
|
+
process.stderr.write("invoke-rest: --out write failed: " +
|
|
1577
|
+
(err instanceof Error ? err.message : String(err)) +
|
|
1578
|
+
"\n");
|
|
1579
|
+
return 1;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1492
1582
|
if (flags.json === "true") {
|
|
1493
1583
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1494
1584
|
}
|
|
@@ -1659,6 +1749,9 @@ async function main() {
|
|
|
1659
1749
|
if (parsed.command === "set-column") {
|
|
1660
1750
|
return await runSetColumn(parsed.flags, parsed.bare);
|
|
1661
1751
|
}
|
|
1752
|
+
if (parsed.command === "set-table") {
|
|
1753
|
+
return await runSetTable(parsed.flags, parsed.bare);
|
|
1754
|
+
}
|
|
1662
1755
|
if (parsed.command === "set-field") {
|
|
1663
1756
|
return await runSetField(parsed.flags);
|
|
1664
1757
|
}
|
|
@@ -1707,13 +1800,57 @@ async function main() {
|
|
|
1707
1800
|
}
|
|
1708
1801
|
throw new Error("Unknown command: " + parsed.command);
|
|
1709
1802
|
}
|
|
1803
|
+
// A closed downstream pipe (e.g. `dove-sn ... --json | head`) surfaces as an
|
|
1804
|
+
// EPIPE on stdout. Exit quietly with the code already set instead of crashing
|
|
1805
|
+
// with an unhandled stream error.
|
|
1806
|
+
process.stdout.on("error", function (err) {
|
|
1807
|
+
if (err && err.code === "EPIPE") {
|
|
1808
|
+
process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
1809
|
+
}
|
|
1810
|
+
throw err;
|
|
1811
|
+
});
|
|
1812
|
+
/**
|
|
1813
|
+
* process.exit() discards buffered stdout/stderr - when stdout is a PIPE,
|
|
1814
|
+
* anything past the OS pipe buffer (~64KB) is silently dropped, which is how
|
|
1815
|
+
* `invoke-rest --json` used to truncate large bodies mid-string. Queue an
|
|
1816
|
+
* empty chunk behind any pending data on each stream and exit only when both
|
|
1817
|
+
* callbacks confirm the flush. The barrier is queued UNCONDITIONALLY (not
|
|
1818
|
+
* gated on writableLength) so the exit never races stream internals about
|
|
1819
|
+
* whether a prior write is still in flight.
|
|
1820
|
+
*/
|
|
1821
|
+
function exitAfterFlush(code) {
|
|
1822
|
+
process.exitCode = code;
|
|
1823
|
+
var pending = 0;
|
|
1824
|
+
var finish = function () {
|
|
1825
|
+
pending -= 1;
|
|
1826
|
+
if (pending <= 0) {
|
|
1827
|
+
process.exit(code);
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
[process.stdout, process.stderr].forEach(function (stream) {
|
|
1831
|
+
if (stream.destroyed || !stream.writable) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
pending += 1;
|
|
1835
|
+
try {
|
|
1836
|
+
stream.write("", finish);
|
|
1837
|
+
}
|
|
1838
|
+
catch (writeErr) {
|
|
1839
|
+
// A stream that rejects the barrier write has nothing left to flush.
|
|
1840
|
+
pending -= 1;
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
if (pending === 0) {
|
|
1844
|
+
process.exit(code);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1710
1847
|
main()
|
|
1711
1848
|
.then(function (code) {
|
|
1712
|
-
|
|
1849
|
+
exitAfterFlush(code);
|
|
1713
1850
|
})
|
|
1714
1851
|
.catch(function (err) {
|
|
1715
1852
|
process.stderr.write("dove-sn error: " +
|
|
1716
1853
|
(err && err.message ? err.message : String(err)) +
|
|
1717
1854
|
"\n");
|
|
1718
|
-
|
|
1855
|
+
exitAfterFlush(1);
|
|
1719
1856
|
});
|
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");
|
package/dist/invokeRest.d.ts
CHANGED
|
@@ -48,3 +48,15 @@ export interface InvokeRestResult {
|
|
|
48
48
|
note: string;
|
|
49
49
|
}
|
|
50
50
|
export declare function invokeRest(params: InvokeRestParams): Promise<InvokeRestResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Write an invoke-rest result to a file as pretty-printed JSON — the
|
|
53
|
+
* first-class landing spot for large responses (piped stdout truncates at the
|
|
54
|
+
* OS pipe buffer if the process exits before draining; see the CLI's
|
|
55
|
+
* flush-aware exit). ATOMIC: writes to a same-directory temp file then
|
|
56
|
+
* renames, so a killed process leaves either the old file or the new one —
|
|
57
|
+
* never a silently-truncated hybrid. Overwrites an existing file by design
|
|
58
|
+
* (documented in --help). The parent directory must already exist; a missing
|
|
59
|
+
* or unwritable directory throws with the underlying error so the CLI can
|
|
60
|
+
* exit 1 loudly instead of "succeeding" with no file.
|
|
61
|
+
*/
|
|
62
|
+
export declare function writeInvokeRestResultFile(outPath: string, result: unknown): void;
|
package/dist/invokeRest.js
CHANGED
|
@@ -17,9 +17,46 @@
|
|
|
17
17
|
* only in the returned result. Any log line about an invocation elsewhere must
|
|
18
18
|
* carry method, path and status ONLY.
|
|
19
19
|
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
20
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
54
|
exports.INVOKE_REST_METHODS = void 0;
|
|
22
55
|
exports.invokeRest = invokeRest;
|
|
56
|
+
exports.writeInvokeRestResultFile = writeInvokeRestResultFile;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
// Aliased: `path` is the instance-relative REST path throughout this module.
|
|
59
|
+
const nodePath = __importStar(require("path"));
|
|
23
60
|
const client_1 = require("./client");
|
|
24
61
|
exports.INVOKE_REST_METHODS = ["GET", "POST", "PUT", "DELETE"];
|
|
25
62
|
function normalizeMethod(method) {
|
|
@@ -91,3 +128,33 @@ async function invokeRest(params) {
|
|
|
91
128
|
}
|
|
92
129
|
return result;
|
|
93
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Write an invoke-rest result to a file as pretty-printed JSON — the
|
|
133
|
+
* first-class landing spot for large responses (piped stdout truncates at the
|
|
134
|
+
* OS pipe buffer if the process exits before draining; see the CLI's
|
|
135
|
+
* flush-aware exit). ATOMIC: writes to a same-directory temp file then
|
|
136
|
+
* renames, so a killed process leaves either the old file or the new one —
|
|
137
|
+
* never a silently-truncated hybrid. Overwrites an existing file by design
|
|
138
|
+
* (documented in --help). The parent directory must already exist; a missing
|
|
139
|
+
* or unwritable directory throws with the underlying error so the CLI can
|
|
140
|
+
* exit 1 loudly instead of "succeeding" with no file.
|
|
141
|
+
*/
|
|
142
|
+
function writeInvokeRestResultFile(outPath, result) {
|
|
143
|
+
var dir = nodePath.dirname(outPath);
|
|
144
|
+
var tempPath = nodePath.join(dir, "." + nodePath.basename(outPath) + ".tmp-" + process.pid + "-" + Date.now());
|
|
145
|
+
var payload = JSON.stringify(result, null, 2) + "\n";
|
|
146
|
+
try {
|
|
147
|
+
fs.writeFileSync(tempPath, payload, { encoding: "utf8" });
|
|
148
|
+
fs.renameSync(tempPath, outPath);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
// Best-effort temp cleanup so a failed write leaves no residue.
|
|
152
|
+
try {
|
|
153
|
+
fs.unlinkSync(tempPath);
|
|
154
|
+
}
|
|
155
|
+
catch (cleanupErr) {
|
|
156
|
+
// The temp file never landed — nothing to clean.
|
|
157
|
+
}
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
package/dist/mcp/registry.d.ts
CHANGED
|
@@ -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({}). */
|
package/dist/mcp/registry.js
CHANGED
|
@@ -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,
|
package/dist/mcp/schemas.d.ts
CHANGED
|
@@ -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>;
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -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
|
package/dist/table/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/table/index.js
CHANGED
|
@@ -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
|
+
}
|