@tenonhq/dovetail-servicenow 0.0.23 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -2
- package/dist/cli.js +119 -0
- package/dist/client.d.ts +2 -2
- package/dist/createRecord.d.ts +33 -0
- package/dist/createRecord.js +137 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -1
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +54 -0
- package/dist/mcp/schemas.d.ts +44 -0
- package/dist/mcp/schemas.js +21 -1
- package/dist/setField.d.ts +50 -0
- package/dist/setField.js +124 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -253,6 +253,72 @@ adds diagnostics (app-switch status, resolved column key, assigned sys_id) to th
|
|
|
253
253
|
result note. Ground truth (the HAR dissection) lives in the CTO repo's create-table
|
|
254
254
|
docs.
|
|
255
255
|
|
|
256
|
+
### Set a field on a record
|
|
257
|
+
|
|
258
|
+
Set scalar field value(s) on an **existing** data record, capture the change into
|
|
259
|
+
an update set, then read it back and verify.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
# Target by sys_id
|
|
263
|
+
npx dove-sn set-field \
|
|
264
|
+
--table x_cadso_core_metric_point_type --sys-id <sys_id> \
|
|
265
|
+
--fields "order=20" --update-set <sys_id> --dry-run --json
|
|
266
|
+
|
|
267
|
+
# Or target by a query that resolves to EXACTLY one row
|
|
268
|
+
npx dove-sn set-field \
|
|
269
|
+
--table x_cadso_core_metric_point_type --query "name=send_size" \
|
|
270
|
+
--fields "order=20,label=Send Size" --update-set <sys_id>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`set-field` wraps the update-set-aware `pushWithUpdateSet` core op (update-set +
|
|
274
|
+
scope switching handled server-side, so no `sys_user_preference` is touched), then
|
|
275
|
+
re-queries the record and verifies each value landed. It **refuses** schema tables
|
|
276
|
+
(`sys_db_object` / `sys_dictionary`) — use `add-column` / `create-table` for those.
|
|
277
|
+
`--fields` is a comma-separated `key=value` map (values are sent as strings;
|
|
278
|
+
ServiceNow coerces); `--update-set` is required so the change is captured;
|
|
279
|
+
`--dry-run` reads the current values and prints the plan without writing. Exit
|
|
280
|
+
codes: `0` applied / dry-run, `1` bad args, `2` write landed but read-back did not
|
|
281
|
+
verify.
|
|
282
|
+
|
|
283
|
+
### Create a record
|
|
284
|
+
|
|
285
|
+
Insert **one** new data record, owned by an explicit scope and captured into an
|
|
286
|
+
update set, then read it back and verify.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
npx dove-sn create-record \
|
|
290
|
+
--table x_cadso_core_metric_point_type \
|
|
291
|
+
--fields "name=avg_message_parts,label=Avg. Message Parts,order=35" \
|
|
292
|
+
--scope x_cadso_core --update-set <sys_id> \
|
|
293
|
+
--if-absent "name=avg_message_parts" --dry-run --json
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`create-record` wraps the scope- and update-set-aware `createRecord` core op, which
|
|
297
|
+
switches the executing user's app scope + update set server-side, inserts, and
|
|
298
|
+
restores both — so the record is owned by the right app and the insert is captured
|
|
299
|
+
in the right update set. Like `set-field` it **refuses** schema tables and verifies
|
|
300
|
+
via read-back. `--scope` and `--update-set` are required; `--if-absent
|
|
301
|
+
"<encoded-query>"` makes re-runs idempotent (the insert is skipped when the query
|
|
302
|
+
already matches a row). Exit codes: `0` created / skipped-in-sync / dry-run, `1` bad
|
|
303
|
+
args, `2` write landed unverified (or skipped with drift). To **update** an existing
|
|
304
|
+
record instead, use `set-field`.
|
|
305
|
+
|
|
306
|
+
Both verbs are exported for programmatic use:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { createClient, setField, createRecord } from "@tenonhq/dovetail-servicenow";
|
|
310
|
+
|
|
311
|
+
var client = createClient({});
|
|
312
|
+
var r = await setField({
|
|
313
|
+
client: client,
|
|
314
|
+
table: "x_cadso_core_metric_point_type",
|
|
315
|
+
sysId: "<sys_id>",
|
|
316
|
+
fields: { order: "20" },
|
|
317
|
+
updateSetSysId: "<sys_id>"
|
|
318
|
+
});
|
|
319
|
+
console.log(r.status, r.verified); // "applied" true
|
|
320
|
+
```
|
|
321
|
+
|
|
256
322
|
`test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
|
|
257
323
|
declared variables?) that never runs the flow; `--execute --confirm` runs it via
|
|
258
324
|
the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
|
|
@@ -323,8 +389,11 @@ console.log(formatLayoutResult("form layout", result));
|
|
|
323
389
|
|
|
324
390
|
`dove-sn mcp` runs a self-contained MCP stdio server exposing the tools to
|
|
325
391
|
Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
|
|
326
|
-
`set_related_lists`, `add_choices_to_field`,
|
|
327
|
-
`
|
|
392
|
+
`set_related_lists`, `add_choices_to_field`, the schema verbs `create_table` /
|
|
393
|
+
`add_column`, the record-write verbs `set_field` (update scalar fields on an
|
|
394
|
+
existing record) and `create_record` (insert one record) — both update-set-captured
|
|
395
|
+
and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow Designer
|
|
396
|
+
tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
|
|
328
397
|
type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
|
|
329
398
|
(copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
|
|
330
399
|
publish, grafting a template), `flow_test` (validate or run a flow), and
|
package/dist/cli.js
CHANGED
|
@@ -79,6 +79,8 @@ const editFlow_1 = require("./flowDesigner/editFlow");
|
|
|
79
79
|
const editActionType_1 = require("./flowDesigner/editActionType");
|
|
80
80
|
const testFlow_1 = require("./flowDesigner/testFlow");
|
|
81
81
|
const table_1 = require("./table");
|
|
82
|
+
const setField_1 = require("./setField");
|
|
83
|
+
const createRecord_1 = require("./createRecord");
|
|
82
84
|
const hostAssets_1 = require("./hostAssets");
|
|
83
85
|
const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
|
|
84
86
|
function parseArgs(argv) {
|
|
@@ -711,6 +713,12 @@ function printHelp() {
|
|
|
711
713
|
" (--table <name|sys_id> --label <l> --type <t>\n" +
|
|
712
714
|
" [--name <element>] [--max-length <n>] [--reference <table>]\n" +
|
|
713
715
|
" [--scope <s>] [--update-set <sys_id>] [--dry-run] [--json])\n" +
|
|
716
|
+
" set-field Set scalar field value(s) on an EXISTING record, into an update set, then verify\n" +
|
|
717
|
+
" (--table <t> --sys-id <id>|--query <q> --fields \"k=v,k2=v2\"\n" +
|
|
718
|
+
" --update-set <sys_id> [--dry-run] [--json])\n" +
|
|
719
|
+
" create-record Create ONE NEW record in a data table, into an update set, then verify\n" +
|
|
720
|
+
" (--table <t> --fields \"k=v,k2=v2\" --scope <s> --update-set <sys_id>\n" +
|
|
721
|
+
" [--if-absent <encoded-query>] [--dry-run] [--json])\n" +
|
|
714
722
|
" host-assets Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)\n" +
|
|
715
723
|
" (--dir <dist> --app <sys_id> --scope <namespace>\n" +
|
|
716
724
|
" [--update-set <sys_id>] [--max-bytes <n>] [--allow-oversize] [--dry-run] [--json])\n" +
|
|
@@ -875,6 +883,111 @@ async function runAddColumn(flags) {
|
|
|
875
883
|
return 2;
|
|
876
884
|
return 0;
|
|
877
885
|
}
|
|
886
|
+
/** Parse inline `--fields "k=v, k2=v2"` into a field map. */
|
|
887
|
+
function parseFieldsInline(input) {
|
|
888
|
+
var out = {};
|
|
889
|
+
if (!input)
|
|
890
|
+
return out;
|
|
891
|
+
var parts = input.split(",");
|
|
892
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
893
|
+
var piece = parts[i].trim();
|
|
894
|
+
if (!piece)
|
|
895
|
+
continue;
|
|
896
|
+
var eq = piece.indexOf("=");
|
|
897
|
+
if (eq === -1)
|
|
898
|
+
continue;
|
|
899
|
+
var key = piece.slice(0, eq).trim();
|
|
900
|
+
if (key)
|
|
901
|
+
out[key] = piece.slice(eq + 1).trim();
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* dove-sn set-field:
|
|
907
|
+
* --table x_cadso_core_metric_point_type
|
|
908
|
+
* --sys-id <id> | --query "name=send_size" (query must resolve to exactly 1 row)
|
|
909
|
+
* --fields "order=20" (comma-separated key=value pairs)
|
|
910
|
+
* --update-set <sys_id> (required — the change is captured here)
|
|
911
|
+
* [--dry-run] [--json]
|
|
912
|
+
* Exit codes: 0 applied/dry-run, 1 bad args, 2 write landed but read-back unverified.
|
|
913
|
+
*/
|
|
914
|
+
async function runSetField(flags) {
|
|
915
|
+
var table = flags.table;
|
|
916
|
+
var fields = parseFieldsInline(flags.fields || "");
|
|
917
|
+
var hasTarget = Boolean(flags["sys-id"] || flags.query);
|
|
918
|
+
if (!table || Object.keys(fields).length === 0 || !hasTarget || !flags["update-set"]) {
|
|
919
|
+
process.stderr.write("set-field: --table, --fields \"k=v\", one of --sys-id/--query, and --update-set are required\n");
|
|
920
|
+
return 1;
|
|
921
|
+
}
|
|
922
|
+
var params = {
|
|
923
|
+
client: (0, client_1.createClient)({}),
|
|
924
|
+
table: table,
|
|
925
|
+
fields: fields,
|
|
926
|
+
updateSetSysId: flags["update-set"]
|
|
927
|
+
};
|
|
928
|
+
if (flags["sys-id"])
|
|
929
|
+
params.sysId = flags["sys-id"];
|
|
930
|
+
if (flags.query)
|
|
931
|
+
params.query = flags.query;
|
|
932
|
+
if (flags["dry-run"] === "true")
|
|
933
|
+
params.dryRun = true;
|
|
934
|
+
var result = await (0, setField_1.setField)(params);
|
|
935
|
+
if (flags.json === "true") {
|
|
936
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
process.stdout.write("[" + result.status + "] " + result.table + "/" + result.sysId + " "
|
|
940
|
+
+ JSON.stringify(result.fields) + (result.verified ? " — verified" : "")
|
|
941
|
+
+ "\n" + result.note + "\n");
|
|
942
|
+
}
|
|
943
|
+
if (result.status === "failed")
|
|
944
|
+
return 2;
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* dove-sn create-record:
|
|
949
|
+
* --table x_cadso_core_metric_point_type
|
|
950
|
+
* --fields "name=avg_message_parts,label=Avg. Message Parts,order=35"
|
|
951
|
+
* --scope x_cadso_core (the app that owns the new record)
|
|
952
|
+
* --update-set <sys_id> (required — the insert is captured here)
|
|
953
|
+
* [--if-absent "name=avg_message_parts"] (skip the insert when this query already matches)
|
|
954
|
+
* [--dry-run] [--json]
|
|
955
|
+
* Exit codes: 0 created/skipped-in-sync/dry-run, 1 bad args, 2 write landed but read-back unverified
|
|
956
|
+
* (or skipped with drift).
|
|
957
|
+
*/
|
|
958
|
+
async function runCreateRecord(flags) {
|
|
959
|
+
var table = flags.table;
|
|
960
|
+
var fields = parseFieldsInline(flags.fields || "");
|
|
961
|
+
if (!table || Object.keys(fields).length === 0 || !flags.scope || !flags["update-set"]) {
|
|
962
|
+
process.stderr.write("create-record: --table, --fields \"k=v\", --scope and --update-set are required\n");
|
|
963
|
+
return 1;
|
|
964
|
+
}
|
|
965
|
+
var params = {
|
|
966
|
+
client: (0, client_1.createClient)({}),
|
|
967
|
+
table: table,
|
|
968
|
+
fields: fields,
|
|
969
|
+
scope: flags.scope,
|
|
970
|
+
updateSetSysId: flags["update-set"]
|
|
971
|
+
};
|
|
972
|
+
if (flags["if-absent"])
|
|
973
|
+
params.ifAbsentQuery = flags["if-absent"];
|
|
974
|
+
if (flags["dry-run"] === "true")
|
|
975
|
+
params.dryRun = true;
|
|
976
|
+
var result = await (0, createRecord_1.createRecord)(params);
|
|
977
|
+
if (flags.json === "true") {
|
|
978
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
process.stdout.write("[" + result.status + "] " + result.table + "/" + (result.sysId || "(new)") + " "
|
|
982
|
+
+ JSON.stringify(result.fields) + (result.verified ? " — verified" : "")
|
|
983
|
+
+ "\n" + result.note + "\n");
|
|
984
|
+
}
|
|
985
|
+
if (result.status === "failed")
|
|
986
|
+
return 2;
|
|
987
|
+
if (result.status === "skipped" && !result.verified)
|
|
988
|
+
return 2;
|
|
989
|
+
return 0;
|
|
990
|
+
}
|
|
878
991
|
/**
|
|
879
992
|
* dove-sn host-assets:
|
|
880
993
|
* --dir <dist> Required. Path to the pre-built dist/ directory.
|
|
@@ -951,6 +1064,12 @@ async function main() {
|
|
|
951
1064
|
if (parsed.command === "add-column") {
|
|
952
1065
|
return await runAddColumn(parsed.flags);
|
|
953
1066
|
}
|
|
1067
|
+
if (parsed.command === "set-field") {
|
|
1068
|
+
return await runSetField(parsed.flags);
|
|
1069
|
+
}
|
|
1070
|
+
if (parsed.command === "create-record") {
|
|
1071
|
+
return await runCreateRecord(parsed.flags);
|
|
1072
|
+
}
|
|
954
1073
|
if (parsed.command === "host-assets") {
|
|
955
1074
|
return await runHostAssets(parsed.flags);
|
|
956
1075
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export interface ServiceNowClient {
|
|
|
74
74
|
/** POST /api/cadso/dovetail_core/createRecord (legacy: /api/cadso/dovetail/createRecord). */
|
|
75
75
|
createRecord: (params: {
|
|
76
76
|
table: string;
|
|
77
|
-
fields: Record<string,
|
|
77
|
+
fields: Record<string, unknown>;
|
|
78
78
|
scope?: string;
|
|
79
79
|
update_set_sys_id?: string;
|
|
80
80
|
sys_id?: string;
|
|
@@ -87,7 +87,7 @@ export interface ServiceNowClient {
|
|
|
87
87
|
update_set_sys_id: string;
|
|
88
88
|
table: string;
|
|
89
89
|
record_sys_id: string;
|
|
90
|
-
fields: Record<string,
|
|
90
|
+
fields: Record<string, unknown>;
|
|
91
91
|
}) => Promise<{
|
|
92
92
|
sys_id: string;
|
|
93
93
|
[k: string]: any;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dove-sn create-record — create ONE new record in a data table, captured
|
|
3
|
+
* into a specified update set, then read back and verify.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the Dovetail core Scripted REST `createRecord` op, which switches the
|
|
6
|
+
* executing user's app scope and update set server-side, inserts, and
|
|
7
|
+
* restores both — so the record is owned by the right app and the insert is
|
|
8
|
+
* captured in the right update set without touching sys_user_preference.
|
|
9
|
+
* The INSERT counterpart to `set-field`.
|
|
10
|
+
*
|
|
11
|
+
* NOT for schema tables (sys_db_object / sys_dictionary) — that's
|
|
12
|
+
* create-table / add-column. To UPDATE an existing record, use `set-field`.
|
|
13
|
+
*/
|
|
14
|
+
import type { ServiceNowClient } from "./client";
|
|
15
|
+
import type { RecordWriteResult } from "./setField";
|
|
16
|
+
export interface CreateRecordParams {
|
|
17
|
+
client?: ServiceNowClient;
|
|
18
|
+
table: string;
|
|
19
|
+
/** Field name -> value for the new record. Values are sent as strings; ServiceNow coerces. */
|
|
20
|
+
fields: Record<string, string>;
|
|
21
|
+
/** App scope that will own the record. Required — session-scope stamping is the #1 wrong-scope cause. */
|
|
22
|
+
scope?: string;
|
|
23
|
+
/** Update set to capture the insert into. Required for a tracked write. */
|
|
24
|
+
updateSetSysId?: string;
|
|
25
|
+
/** Encoded query; when it already matches a row the insert is skipped (idempotent re-runs). */
|
|
26
|
+
ifAbsentQuery?: string;
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface CreateRecordResult extends RecordWriteResult {
|
|
30
|
+
status: "dry-run" | "created" | "skipped" | "failed";
|
|
31
|
+
scope: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function createRecord(params: CreateRecordParams): Promise<CreateRecordResult>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dove-sn create-record — create ONE new record in a data table, captured
|
|
4
|
+
* into a specified update set, then read back and verify.
|
|
5
|
+
*
|
|
6
|
+
* Wraps the Dovetail core Scripted REST `createRecord` op, which switches the
|
|
7
|
+
* executing user's app scope and update set server-side, inserts, and
|
|
8
|
+
* restores both — so the record is owned by the right app and the insert is
|
|
9
|
+
* captured in the right update set without touching sys_user_preference.
|
|
10
|
+
* The INSERT counterpart to `set-field`.
|
|
11
|
+
*
|
|
12
|
+
* NOT for schema tables (sys_db_object / sys_dictionary) — that's
|
|
13
|
+
* create-table / add-column. To UPDATE an existing record, use `set-field`.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.createRecord = createRecord;
|
|
17
|
+
const client_1 = require("./client");
|
|
18
|
+
const setField_1 = require("./setField");
|
|
19
|
+
// Same guard as set-field: schema tables are never written as data — routed to
|
|
20
|
+
// the dedicated schema verbs instead so we never orphan or corrupt metadata.
|
|
21
|
+
var REFUSED_TABLES = ["sys_db_object", "sys_dictionary"];
|
|
22
|
+
async function createRecord(params) {
|
|
23
|
+
var client = params.client || (0, client_1.createClient)({});
|
|
24
|
+
var table = params.table;
|
|
25
|
+
if (!table) {
|
|
26
|
+
throw new Error("create-record: --table is required.");
|
|
27
|
+
}
|
|
28
|
+
if (REFUSED_TABLES.indexOf(table) !== -1) {
|
|
29
|
+
throw new Error("create-record: refusing to write " + table + " as data — it is a schema table. "
|
|
30
|
+
+ "Use add-column / create-table for schema changes.");
|
|
31
|
+
}
|
|
32
|
+
var fieldNames = params.fields ? Object.keys(params.fields) : [];
|
|
33
|
+
if (fieldNames.length === 0) {
|
|
34
|
+
throw new Error("create-record: at least one field (--fields key=value) is required.");
|
|
35
|
+
}
|
|
36
|
+
if (!params.scope) {
|
|
37
|
+
throw new Error("create-record: --scope is required so the record is owned by the right app.");
|
|
38
|
+
}
|
|
39
|
+
if (!params.updateSetSysId) {
|
|
40
|
+
throw new Error("create-record: --update-set <sys_id> is required so the insert is captured.");
|
|
41
|
+
}
|
|
42
|
+
var readFields = ["sys_id"].concat(fieldNames);
|
|
43
|
+
// Idempotent re-runs: skip the insert when the guard query already matches.
|
|
44
|
+
if (params.ifAbsentQuery) {
|
|
45
|
+
var existing = await client.table.query(table, params.ifAbsentQuery, {
|
|
46
|
+
limit: 1,
|
|
47
|
+
fields: readFields
|
|
48
|
+
});
|
|
49
|
+
if (existing.length > 0) {
|
|
50
|
+
var existingSysId = (0, setField_1.fieldToString)(existing[0].sys_id);
|
|
51
|
+
var existingValues = (0, setField_1.pickFields)(existing[0], fieldNames);
|
|
52
|
+
var matches = true;
|
|
53
|
+
for (var i = 0; i < fieldNames.length; i += 1) {
|
|
54
|
+
if (existingValues[fieldNames[i]] !== (0, setField_1.fieldToString)(params.fields[fieldNames[i]])) {
|
|
55
|
+
matches = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
status: "skipped",
|
|
60
|
+
table: table,
|
|
61
|
+
sysId: existingSysId,
|
|
62
|
+
scope: params.scope,
|
|
63
|
+
updateSetSysId: params.updateSetSysId,
|
|
64
|
+
fields: params.fields,
|
|
65
|
+
after: existingValues,
|
|
66
|
+
verified: matches,
|
|
67
|
+
note: matches
|
|
68
|
+
? "skipped: --if-absent matched " + table + "/" + existingSysId
|
|
69
|
+
+ " and its values already match."
|
|
70
|
+
: "skipped: --if-absent matched " + table + "/" + existingSysId
|
|
71
|
+
+ " but its values differ from the requested fields — use set-field to update it."
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (params.dryRun) {
|
|
76
|
+
return {
|
|
77
|
+
status: "dry-run",
|
|
78
|
+
table: table,
|
|
79
|
+
sysId: "",
|
|
80
|
+
scope: params.scope,
|
|
81
|
+
updateSetSysId: params.updateSetSysId,
|
|
82
|
+
fields: params.fields,
|
|
83
|
+
after: {},
|
|
84
|
+
verified: false,
|
|
85
|
+
note: "dry-run: no write. Would create a " + table + " record with "
|
|
86
|
+
+ JSON.stringify(params.fields) + " in scope " + params.scope
|
|
87
|
+
+ ", captured into update set " + params.updateSetSysId + "."
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Insert via the scope- and update-set-aware core REST op.
|
|
91
|
+
var created = await client.claude.createRecord({
|
|
92
|
+
table: table,
|
|
93
|
+
fields: params.fields,
|
|
94
|
+
scope: params.scope,
|
|
95
|
+
update_set_sys_id: params.updateSetSysId
|
|
96
|
+
});
|
|
97
|
+
var sysId = (0, setField_1.fieldToString)(created && created.sys_id);
|
|
98
|
+
if (!sysId) {
|
|
99
|
+
return {
|
|
100
|
+
status: "failed",
|
|
101
|
+
table: table,
|
|
102
|
+
sysId: "",
|
|
103
|
+
scope: params.scope,
|
|
104
|
+
updateSetSysId: params.updateSetSysId,
|
|
105
|
+
fields: params.fields,
|
|
106
|
+
after: {},
|
|
107
|
+
verified: false,
|
|
108
|
+
note: "createRecord returned no sys_id — the insert may not have landed; check the instance."
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Read back and verify each field equals what we sent.
|
|
112
|
+
var afterRows = await client.table.query(table, "sys_id=" + sysId, {
|
|
113
|
+
limit: 1,
|
|
114
|
+
fields: readFields
|
|
115
|
+
});
|
|
116
|
+
var after = (0, setField_1.pickFields)(afterRows[0] || {}, fieldNames);
|
|
117
|
+
var verified = afterRows.length > 0;
|
|
118
|
+
for (var j = 0; j < fieldNames.length; j += 1) {
|
|
119
|
+
if (after[fieldNames[j]] !== (0, setField_1.fieldToString)(params.fields[fieldNames[j]])) {
|
|
120
|
+
verified = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
status: verified ? "created" : "failed",
|
|
125
|
+
table: table,
|
|
126
|
+
sysId: sysId,
|
|
127
|
+
scope: params.scope,
|
|
128
|
+
updateSetSysId: params.updateSetSysId,
|
|
129
|
+
fields: params.fields,
|
|
130
|
+
after: after,
|
|
131
|
+
verified: verified,
|
|
132
|
+
note: verified
|
|
133
|
+
? "Created " + table + "/" + sysId + " and verified via read-back."
|
|
134
|
+
: "Insert landed as " + sysId
|
|
135
|
+
+ " but read-back does not match the requested values — check field types / ACLs."
|
|
136
|
+
};
|
|
137
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,3 +21,7 @@ export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFo
|
|
|
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
22
|
export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, applyAddColumnOverlay } from "./table";
|
|
23
23
|
export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec } from "./table";
|
|
24
|
+
export { setField } from "./setField";
|
|
25
|
+
export { createRecord } from "./createRecord";
|
|
26
|
+
export type { RecordWriteResult, SetFieldParams, SetFieldResult } from "./setField";
|
|
27
|
+
export type { CreateRecordParams, CreateRecordResult } from "./createRecord";
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* REST API so every change lands in the target update set and scope.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.applyAddColumnOverlay = exports.deriveElement = 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.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;
|
|
9
|
+
exports.createRecord = exports.setField = exports.applyAddColumnOverlay = exports.deriveElement = 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.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
10
|
var client_1 = require("./client");
|
|
11
11
|
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
|
|
12
12
|
var createClientFromEnvFile_1 = require("./createClientFromEnvFile");
|
|
@@ -68,3 +68,7 @@ Object.defineProperty(exports, "DEFAULT_SAVE_ACTION", { enumerable: true, get: f
|
|
|
68
68
|
Object.defineProperty(exports, "addColumn", { enumerable: true, get: function () { return table_1.addColumn; } });
|
|
69
69
|
Object.defineProperty(exports, "deriveElement", { enumerable: true, get: function () { return table_1.deriveElement; } });
|
|
70
70
|
Object.defineProperty(exports, "applyAddColumnOverlay", { enumerable: true, get: function () { return table_1.applyAddColumnOverlay; } });
|
|
71
|
+
var setField_1 = require("./setField");
|
|
72
|
+
Object.defineProperty(exports, "setField", { enumerable: true, get: function () { return setField_1.setField; } });
|
|
73
|
+
var createRecord_1 = require("./createRecord");
|
|
74
|
+
Object.defineProperty(exports, "createRecord", { enumerable: true, get: function () { return createRecord_1.createRecord; } });
|
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", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "host_assets"];
|
|
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", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_field", "create_record", "host_assets"];
|
|
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
|
@@ -27,6 +27,8 @@ const editFlow_1 = require("../flowDesigner/editFlow");
|
|
|
27
27
|
const testFlow_1 = require("../flowDesigner/testFlow");
|
|
28
28
|
const table_1 = require("../table");
|
|
29
29
|
const hostAssets_1 = require("../hostAssets");
|
|
30
|
+
const setField_1 = require("../setField");
|
|
31
|
+
const createRecord_1 = require("../createRecord");
|
|
30
32
|
const schemas_1 = require("./schemas");
|
|
31
33
|
exports.TOOL_NAMES = [
|
|
32
34
|
"create_view",
|
|
@@ -43,6 +45,8 @@ exports.TOOL_NAMES = [
|
|
|
43
45
|
"flow_edit",
|
|
44
46
|
"create_table",
|
|
45
47
|
"add_column",
|
|
48
|
+
"set_field",
|
|
49
|
+
"create_record",
|
|
46
50
|
"host_assets"
|
|
47
51
|
];
|
|
48
52
|
// Annotation presets (READ_ONLY / WRITE_ADDITIVE_IDEMPOTENT / WRITE_CREATE /
|
|
@@ -296,6 +300,56 @@ function buildDescriptors(deps = {}) {
|
|
|
296
300
|
});
|
|
297
301
|
}
|
|
298
302
|
},
|
|
303
|
+
{
|
|
304
|
+
name: "set_field",
|
|
305
|
+
annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
|
|
306
|
+
description: "Set scalar field value(s) on an EXISTING ServiceNow data record, captured into a specified "
|
|
307
|
+
+ "update set, then READ BACK to verify each value landed. Wraps the update-set-aware "
|
|
308
|
+
+ "pushWithUpdateSet core op (no sys_user_preference mutation). Target the record by sysId, or "
|
|
309
|
+
+ "by a query that resolves to EXACTLY one row. REFUSES schema tables (sys_db_object / "
|
|
310
|
+
+ "sys_dictionary) — use add_column / create_table for those. fields is a flat name->string map "
|
|
311
|
+
+ "(sent as strings; ServiceNow coerces); updateSetSysId is required so the change is tracked; "
|
|
312
|
+
+ "dryRun:true reads the current values and returns the plan without writing. To INSERT a new "
|
|
313
|
+
+ "record use create_record.",
|
|
314
|
+
shape: schemas_1.setFieldSchema.shape,
|
|
315
|
+
handler: async function (args) {
|
|
316
|
+
var p = schemas_1.setFieldSchema.parse(args);
|
|
317
|
+
return (0, setField_1.setField)({
|
|
318
|
+
client: client(),
|
|
319
|
+
table: p.table,
|
|
320
|
+
sysId: p.sysId,
|
|
321
|
+
query: p.query,
|
|
322
|
+
fields: p.fields,
|
|
323
|
+
updateSetSysId: p.updateSetSysId,
|
|
324
|
+
dryRun: p.dryRun
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "create_record",
|
|
330
|
+
annotations: dovetail_mcp_kit_1.WRITE_CREATE,
|
|
331
|
+
description: "Create ONE new ServiceNow data record, owned by an explicit app scope and captured into a "
|
|
332
|
+
+ "specified update set, then READ BACK to verify. Wraps the scope- and update-set-aware "
|
|
333
|
+
+ "createRecord core op (switches app scope + update set server-side, inserts, restores both — "
|
|
334
|
+
+ "so the record lands in the right scope without sys_user_preference mutation). REFUSES schema "
|
|
335
|
+
+ "tables (sys_db_object / sys_dictionary) — use create_table / add_column for those. fields is a "
|
|
336
|
+
+ "flat name->string map; scope and updateSetSysId are required; ifAbsentQuery makes re-runs "
|
|
337
|
+
+ "idempotent (skips the insert when it already matches a row); dryRun:true returns the plan "
|
|
338
|
+
+ "without writing. To UPDATE an existing record use set_field.",
|
|
339
|
+
shape: schemas_1.createRecordSchema.shape,
|
|
340
|
+
handler: async function (args) {
|
|
341
|
+
var p = schemas_1.createRecordSchema.parse(args);
|
|
342
|
+
return (0, createRecord_1.createRecord)({
|
|
343
|
+
client: client(),
|
|
344
|
+
table: p.table,
|
|
345
|
+
fields: p.fields,
|
|
346
|
+
scope: p.scope,
|
|
347
|
+
updateSetSysId: p.updateSetSysId,
|
|
348
|
+
ifAbsentQuery: p.ifAbsentQuery,
|
|
349
|
+
dryRun: p.dryRun
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
},
|
|
299
353
|
{
|
|
300
354
|
name: "host_assets",
|
|
301
355
|
annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
|
package/dist/mcp/schemas.d.ts
CHANGED
|
@@ -563,3 +563,47 @@ export declare var addColumnSchema: z.ZodObject<{
|
|
|
563
563
|
saveActionSysId?: string | undefined;
|
|
564
564
|
columnsRelId?: string | undefined;
|
|
565
565
|
}>;
|
|
566
|
+
export declare var setFieldSchema: z.ZodObject<{
|
|
567
|
+
table: z.ZodString;
|
|
568
|
+
sysId: z.ZodOptional<z.ZodString>;
|
|
569
|
+
query: z.ZodOptional<z.ZodString>;
|
|
570
|
+
fields: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
571
|
+
updateSetSysId: z.ZodString;
|
|
572
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
573
|
+
}, "strip", z.ZodTypeAny, {
|
|
574
|
+
table: string;
|
|
575
|
+
fields: Record<string, string>;
|
|
576
|
+
updateSetSysId: string;
|
|
577
|
+
query?: string | undefined;
|
|
578
|
+
sysId?: string | undefined;
|
|
579
|
+
dryRun?: boolean | undefined;
|
|
580
|
+
}, {
|
|
581
|
+
table: string;
|
|
582
|
+
fields: Record<string, string>;
|
|
583
|
+
updateSetSysId: string;
|
|
584
|
+
query?: string | undefined;
|
|
585
|
+
sysId?: string | undefined;
|
|
586
|
+
dryRun?: boolean | undefined;
|
|
587
|
+
}>;
|
|
588
|
+
export declare var createRecordSchema: z.ZodObject<{
|
|
589
|
+
table: z.ZodString;
|
|
590
|
+
fields: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
591
|
+
scope: z.ZodString;
|
|
592
|
+
updateSetSysId: z.ZodString;
|
|
593
|
+
ifAbsentQuery: z.ZodOptional<z.ZodString>;
|
|
594
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
595
|
+
}, "strip", z.ZodTypeAny, {
|
|
596
|
+
table: string;
|
|
597
|
+
fields: Record<string, string>;
|
|
598
|
+
scope: string;
|
|
599
|
+
updateSetSysId: string;
|
|
600
|
+
dryRun?: boolean | undefined;
|
|
601
|
+
ifAbsentQuery?: string | undefined;
|
|
602
|
+
}, {
|
|
603
|
+
table: string;
|
|
604
|
+
fields: Record<string, string>;
|
|
605
|
+
scope: string;
|
|
606
|
+
updateSetSysId: string;
|
|
607
|
+
dryRun?: boolean | undefined;
|
|
608
|
+
ifAbsentQuery?: string | undefined;
|
|
609
|
+
}>;
|
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.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = 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.createRecordSchema = exports.setFieldSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = 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),
|
|
@@ -156,3 +156,23 @@ exports.addColumnSchema = zod_1.z.object({
|
|
|
156
156
|
dryRun: zod_1.z.boolean().optional(),
|
|
157
157
|
debug: zod_1.z.boolean().optional()
|
|
158
158
|
});
|
|
159
|
+
// Data-record write verbs. Kept as plain z.object (no .refine wrapper) so
|
|
160
|
+
// registry.ts can read `.shape`; the deeper rules — one of sysId/query, at
|
|
161
|
+
// least one field, the schema-table refusal — are enforced by the core
|
|
162
|
+
// setField / createRecord functions, which throw clear errors.
|
|
163
|
+
exports.setFieldSchema = zod_1.z.object({
|
|
164
|
+
table: zod_1.z.string().min(1),
|
|
165
|
+
sysId: zod_1.z.string().optional(),
|
|
166
|
+
query: zod_1.z.string().optional(),
|
|
167
|
+
fields: zod_1.z.record(zod_1.z.string()),
|
|
168
|
+
updateSetSysId: zod_1.z.string().min(1),
|
|
169
|
+
dryRun: zod_1.z.boolean().optional()
|
|
170
|
+
});
|
|
171
|
+
exports.createRecordSchema = zod_1.z.object({
|
|
172
|
+
table: zod_1.z.string().min(1),
|
|
173
|
+
fields: zod_1.z.record(zod_1.z.string()),
|
|
174
|
+
scope: zod_1.z.string().min(1),
|
|
175
|
+
updateSetSysId: zod_1.z.string().min(1),
|
|
176
|
+
ifAbsentQuery: zod_1.z.string().optional(),
|
|
177
|
+
dryRun: zod_1.z.boolean().optional()
|
|
178
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dove-sn set-field — set scalar field value(s) on an EXISTING ServiceNow
|
|
3
|
+
* record, captured into a specified update set, then read back and verify.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the Dovetail core Scripted REST `pushWithUpdateSet` op (update-set +
|
|
6
|
+
* scope switching handled atomically server-side), so the change lands in the
|
|
7
|
+
* right update set without touching sys_user_preference. This is the
|
|
8
|
+
* change-and-KEEP counterpart to sn-capture-fields' change-and-revert capture.
|
|
9
|
+
*
|
|
10
|
+
* NOT for schema tables (sys_db_object / sys_dictionary) — that's add-column /
|
|
11
|
+
* create-table. To INSERT a new record, use `dove create`.
|
|
12
|
+
*/
|
|
13
|
+
import type { ServiceNowClient } from "./client";
|
|
14
|
+
export interface SetFieldParams {
|
|
15
|
+
client?: ServiceNowClient;
|
|
16
|
+
table: string;
|
|
17
|
+
/** Target record by sys_id, OR by a query that resolves to EXACTLY one row. */
|
|
18
|
+
sysId?: string;
|
|
19
|
+
query?: string;
|
|
20
|
+
/** Field name -> value to set. Values are sent as strings; ServiceNow coerces. */
|
|
21
|
+
fields: Record<string, string>;
|
|
22
|
+
/** Update set to capture the change into. Required for a tracked write. */
|
|
23
|
+
updateSetSysId?: string;
|
|
24
|
+
dryRun?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fields common to every record-write verb's result: the target, the update set
|
|
28
|
+
* the change was captured into, the requested field map, the read-back values,
|
|
29
|
+
* whether the read-back matched, and a human note. `set-field` and
|
|
30
|
+
* `create-record` each extend this with their own `status` union (and
|
|
31
|
+
* set-field's `before` snapshot / create-record's owning `scope`).
|
|
32
|
+
*/
|
|
33
|
+
export interface RecordWriteResult {
|
|
34
|
+
table: string;
|
|
35
|
+
sysId: string;
|
|
36
|
+
updateSetSysId: string;
|
|
37
|
+
fields: Record<string, string>;
|
|
38
|
+
after: Record<string, string>;
|
|
39
|
+
verified: boolean;
|
|
40
|
+
note: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SetFieldResult extends RecordWriteResult {
|
|
43
|
+
status: "dry-run" | "applied" | "failed";
|
|
44
|
+
before: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
/** Coerce a Table-API field value to a comparable string. Reference/display
|
|
47
|
+
* fields (sysparm_display_value=false) come back as { link, value } objects. */
|
|
48
|
+
export declare function fieldToString(value: unknown): string;
|
|
49
|
+
export declare function pickFields(row: Record<string, unknown>, names: Array<string>): Record<string, string>;
|
|
50
|
+
export declare function setField(params: SetFieldParams): Promise<SetFieldResult>;
|
package/dist/setField.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dove-sn set-field — set scalar field value(s) on an EXISTING ServiceNow
|
|
4
|
+
* record, captured into a specified update set, then read back and verify.
|
|
5
|
+
*
|
|
6
|
+
* Wraps the Dovetail core Scripted REST `pushWithUpdateSet` op (update-set +
|
|
7
|
+
* scope switching handled atomically server-side), so the change lands in the
|
|
8
|
+
* right update set without touching sys_user_preference. This is the
|
|
9
|
+
* change-and-KEEP counterpart to sn-capture-fields' change-and-revert capture.
|
|
10
|
+
*
|
|
11
|
+
* NOT for schema tables (sys_db_object / sys_dictionary) — that's add-column /
|
|
12
|
+
* create-table. To INSERT a new record, use `dove create`.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.fieldToString = fieldToString;
|
|
16
|
+
exports.pickFields = pickFields;
|
|
17
|
+
exports.setField = setField;
|
|
18
|
+
const client_1 = require("./client");
|
|
19
|
+
// Platform/schema tables that must not be written as data — routed to the
|
|
20
|
+
// dedicated schema verbs instead so we never orphan or corrupt metadata.
|
|
21
|
+
var REFUSED_TABLES = ["sys_db_object", "sys_dictionary"];
|
|
22
|
+
/** Coerce a Table-API field value to a comparable string. Reference/display
|
|
23
|
+
* fields (sysparm_display_value=false) come back as { link, value } objects. */
|
|
24
|
+
function fieldToString(value) {
|
|
25
|
+
if (value === undefined || value === null)
|
|
26
|
+
return "";
|
|
27
|
+
if (typeof value === "object") {
|
|
28
|
+
if ("value" in value && value.value !== undefined && value.value !== null) {
|
|
29
|
+
return String(value.value);
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
function pickFields(row, names) {
|
|
36
|
+
var out = {};
|
|
37
|
+
for (var i = 0; i < names.length; i += 1) {
|
|
38
|
+
out[names[i]] = fieldToString(row ? row[names[i]] : undefined);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
async function setField(params) {
|
|
43
|
+
var client = params.client || (0, client_1.createClient)({});
|
|
44
|
+
var table = params.table;
|
|
45
|
+
if (!table) {
|
|
46
|
+
throw new Error("set-field: --table is required.");
|
|
47
|
+
}
|
|
48
|
+
if (REFUSED_TABLES.indexOf(table) !== -1) {
|
|
49
|
+
throw new Error("set-field: refusing to write " + table + " as data — it is a schema table. "
|
|
50
|
+
+ "Use add-column / create-table for schema changes.");
|
|
51
|
+
}
|
|
52
|
+
var fieldNames = params.fields ? Object.keys(params.fields) : [];
|
|
53
|
+
if (fieldNames.length === 0) {
|
|
54
|
+
throw new Error("set-field: at least one field (--fields key=value) is required.");
|
|
55
|
+
}
|
|
56
|
+
if (!params.updateSetSysId) {
|
|
57
|
+
throw new Error("set-field: --update-set <sys_id> is required so the change is captured.");
|
|
58
|
+
}
|
|
59
|
+
// Resolve the target sys_id (explicit, or a single-match query).
|
|
60
|
+
var sysId = params.sysId;
|
|
61
|
+
if (!sysId) {
|
|
62
|
+
if (!params.query) {
|
|
63
|
+
throw new Error("set-field: one of --sys-id or --query is required.");
|
|
64
|
+
}
|
|
65
|
+
var matches = await client.table.query(table, params.query, { limit: 2, fields: ["sys_id"] });
|
|
66
|
+
if (matches.length === 0) {
|
|
67
|
+
throw new Error("set-field: --query matched no rows on " + table + ".");
|
|
68
|
+
}
|
|
69
|
+
if (matches.length > 1) {
|
|
70
|
+
throw new Error("set-field: --query matched 2+ rows on " + table + " — refine to exactly one.");
|
|
71
|
+
}
|
|
72
|
+
sysId = fieldToString(matches[0].sys_id);
|
|
73
|
+
}
|
|
74
|
+
// Read current values (also confirms the record exists).
|
|
75
|
+
var readFields = ["sys_id"].concat(fieldNames);
|
|
76
|
+
var beforeRows = await client.table.query(table, "sys_id=" + sysId, { limit: 1, fields: readFields });
|
|
77
|
+
if (beforeRows.length === 0) {
|
|
78
|
+
throw new Error("set-field: no record " + sysId + " found on " + table + ".");
|
|
79
|
+
}
|
|
80
|
+
var before = pickFields(beforeRows[0], fieldNames);
|
|
81
|
+
if (params.dryRun) {
|
|
82
|
+
return {
|
|
83
|
+
status: "dry-run",
|
|
84
|
+
table: table,
|
|
85
|
+
sysId: sysId,
|
|
86
|
+
updateSetSysId: params.updateSetSysId,
|
|
87
|
+
fields: params.fields,
|
|
88
|
+
before: before,
|
|
89
|
+
after: before,
|
|
90
|
+
verified: false,
|
|
91
|
+
note: "dry-run: no write. Would set " + JSON.stringify(params.fields)
|
|
92
|
+
+ " on " + table + "/" + sysId + " into update set " + params.updateSetSysId + "."
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Write via the update-set-aware core REST op.
|
|
96
|
+
await client.claude.pushWithUpdateSet({
|
|
97
|
+
update_set_sys_id: params.updateSetSysId,
|
|
98
|
+
table: table,
|
|
99
|
+
record_sys_id: sysId,
|
|
100
|
+
fields: params.fields
|
|
101
|
+
});
|
|
102
|
+
// Read back and verify each field equals what we set.
|
|
103
|
+
var afterRows = await client.table.query(table, "sys_id=" + sysId, { limit: 1, fields: readFields });
|
|
104
|
+
var after = pickFields(afterRows[0] || {}, fieldNames);
|
|
105
|
+
var verified = true;
|
|
106
|
+
for (var j = 0; j < fieldNames.length; j += 1) {
|
|
107
|
+
if (after[fieldNames[j]] !== fieldToString(params.fields[fieldNames[j]])) {
|
|
108
|
+
verified = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
status: verified ? "applied" : "failed",
|
|
113
|
+
table: table,
|
|
114
|
+
sysId: sysId,
|
|
115
|
+
updateSetSysId: params.updateSetSysId,
|
|
116
|
+
fields: params.fields,
|
|
117
|
+
before: before,
|
|
118
|
+
after: after,
|
|
119
|
+
verified: verified,
|
|
120
|
+
note: verified
|
|
121
|
+
? "Set " + fieldNames.join(", ") + " on " + table + "/" + sysId + " and verified via read-back."
|
|
122
|
+
: "Write landed but read-back does not match the requested values — check field types / ACLs."
|
|
123
|
+
};
|
|
124
|
+
}
|