@tenonhq/dovetail-servicenow 0.0.23 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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
  }
@@ -0,0 +1,39 @@
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
+ export interface CreateRecordParams {
16
+ client?: ServiceNowClient;
17
+ table: string;
18
+ /** Field name -> value for the new record. Values are sent as strings; ServiceNow coerces. */
19
+ fields: Record<string, string>;
20
+ /** App scope that will own the record. Required — session-scope stamping is the #1 wrong-scope cause. */
21
+ scope?: string;
22
+ /** Update set to capture the insert into. Required for a tracked write. */
23
+ updateSetSysId?: string;
24
+ /** Encoded query; when it already matches a row the insert is skipped (idempotent re-runs). */
25
+ ifAbsentQuery?: string;
26
+ dryRun?: boolean;
27
+ }
28
+ export interface CreateRecordResult {
29
+ status: "dry-run" | "created" | "skipped" | "failed";
30
+ table: string;
31
+ sysId: string;
32
+ scope: string;
33
+ updateSetSysId: string;
34
+ fields: Record<string, string>;
35
+ after: Record<string, string>;
36
+ verified: boolean;
37
+ note: string;
38
+ }
39
+ 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
+ }
@@ -0,0 +1,41 @@
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
+ export interface SetFieldResult {
27
+ status: "dry-run" | "applied" | "failed";
28
+ table: string;
29
+ sysId: string;
30
+ updateSetSysId: string;
31
+ fields: Record<string, string>;
32
+ before: Record<string, string>;
33
+ after: Record<string, string>;
34
+ verified: boolean;
35
+ note: string;
36
+ }
37
+ /** Coerce a Table-API field value to a comparable string. Reference/display
38
+ * fields (sysparm_display_value=false) come back as { link, value } objects. */
39
+ export declare function fieldToString(value: any): string;
40
+ export declare function pickFields(row: Record<string, any>, names: Array<string>): Record<string, string>;
41
+ export declare function setField(params: SetFieldParams): Promise<SetFieldResult>;
@@ -0,0 +1,121 @@
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
+ return value.value !== undefined && value.value !== null ? String(value.value) : "";
29
+ }
30
+ return String(value);
31
+ }
32
+ function pickFields(row, names) {
33
+ var out = {};
34
+ for (var i = 0; i < names.length; i += 1) {
35
+ out[names[i]] = fieldToString(row ? row[names[i]] : undefined);
36
+ }
37
+ return out;
38
+ }
39
+ async function setField(params) {
40
+ var client = params.client || (0, client_1.createClient)({});
41
+ var table = params.table;
42
+ if (!table) {
43
+ throw new Error("set-field: --table is required.");
44
+ }
45
+ if (REFUSED_TABLES.indexOf(table) !== -1) {
46
+ throw new Error("set-field: refusing to write " + table + " as data — it is a schema table. "
47
+ + "Use add-column / create-table for schema changes.");
48
+ }
49
+ var fieldNames = params.fields ? Object.keys(params.fields) : [];
50
+ if (fieldNames.length === 0) {
51
+ throw new Error("set-field: at least one field (--fields key=value) is required.");
52
+ }
53
+ if (!params.updateSetSysId) {
54
+ throw new Error("set-field: --update-set <sys_id> is required so the change is captured.");
55
+ }
56
+ // Resolve the target sys_id (explicit, or a single-match query).
57
+ var sysId = params.sysId;
58
+ if (!sysId) {
59
+ if (!params.query) {
60
+ throw new Error("set-field: one of --sys-id or --query is required.");
61
+ }
62
+ var matches = await client.table.query(table, params.query, { limit: 2, fields: ["sys_id"] });
63
+ if (matches.length === 0) {
64
+ throw new Error("set-field: --query matched no rows on " + table + ".");
65
+ }
66
+ if (matches.length > 1) {
67
+ throw new Error("set-field: --query matched 2+ rows on " + table + " — refine to exactly one.");
68
+ }
69
+ sysId = fieldToString(matches[0].sys_id);
70
+ }
71
+ // Read current values (also confirms the record exists).
72
+ var readFields = ["sys_id"].concat(fieldNames);
73
+ var beforeRows = await client.table.query(table, "sys_id=" + sysId, { limit: 1, fields: readFields });
74
+ if (beforeRows.length === 0) {
75
+ throw new Error("set-field: no record " + sysId + " found on " + table + ".");
76
+ }
77
+ var before = pickFields(beforeRows[0], fieldNames);
78
+ if (params.dryRun) {
79
+ return {
80
+ status: "dry-run",
81
+ table: table,
82
+ sysId: sysId,
83
+ updateSetSysId: params.updateSetSysId,
84
+ fields: params.fields,
85
+ before: before,
86
+ after: before,
87
+ verified: false,
88
+ note: "dry-run: no write. Would set " + JSON.stringify(params.fields)
89
+ + " on " + table + "/" + sysId + " into update set " + params.updateSetSysId + "."
90
+ };
91
+ }
92
+ // Write via the update-set-aware core REST op.
93
+ await client.claude.pushWithUpdateSet({
94
+ update_set_sys_id: params.updateSetSysId,
95
+ table: table,
96
+ record_sys_id: sysId,
97
+ fields: params.fields
98
+ });
99
+ // Read back and verify each field equals what we set.
100
+ var afterRows = await client.table.query(table, "sys_id=" + sysId, { limit: 1, fields: readFields });
101
+ var after = pickFields(afterRows[0] || {}, fieldNames);
102
+ var verified = true;
103
+ for (var j = 0; j < fieldNames.length; j += 1) {
104
+ if (after[fieldNames[j]] !== fieldToString(params.fields[fieldNames[j]])) {
105
+ verified = false;
106
+ }
107
+ }
108
+ return {
109
+ status: verified ? "applied" : "failed",
110
+ table: table,
111
+ sysId: sysId,
112
+ updateSetSysId: params.updateSetSysId,
113
+ fields: params.fields,
114
+ before: before,
115
+ after: after,
116
+ verified: verified,
117
+ note: verified
118
+ ? "Set " + fieldNames.join(", ") + " on " + table + "/" + sysId + " and verified via read-back."
119
+ : "Write landed but read-back does not match the requested values — check field types / ACLs."
120
+ };
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },