@tenonhq/dovetail-servicenow 0.0.22 → 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 +168 -0
- package/dist/client.d.ts +35 -0
- package/dist/client.js +31 -0
- package/dist/createRecord.d.ts +39 -0
- package/dist/createRecord.js +137 -0
- package/dist/hostAssets.d.ts +44 -0
- package/dist/hostAssets.js +579 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -1
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +21 -1
- package/dist/mcp/schemas.d.ts +25 -0
- package/dist/mcp/schemas.js +10 -1
- package/dist/setField.d.ts +41 -0
- package/dist/setField.js +121 -0
- package/dist/types.d.ts +81 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -79,6 +79,9 @@ 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");
|
|
84
|
+
const hostAssets_1 = require("./hostAssets");
|
|
82
85
|
const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
|
|
83
86
|
function parseArgs(argv) {
|
|
84
87
|
var command = argv[0] || "";
|
|
@@ -710,6 +713,15 @@ function printHelp() {
|
|
|
710
713
|
" (--table <name|sys_id> --label <l> --type <t>\n" +
|
|
711
714
|
" [--name <element>] [--max-length <n>] [--reference <table>]\n" +
|
|
712
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" +
|
|
722
|
+
" host-assets Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)\n" +
|
|
723
|
+
" (--dir <dist> --app <sys_id> --scope <namespace>\n" +
|
|
724
|
+
" [--update-set <sys_id>] [--max-bytes <n>] [--allow-oversize] [--dry-run] [--json])\n" +
|
|
713
725
|
" test-flow Validate (default) or run a flow/subflow\n" +
|
|
714
726
|
" (--sys-id <sys_id> [--execute --confirm] [--inputs <json>] [--json])\n" +
|
|
715
727
|
" edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
|
|
@@ -871,6 +883,153 @@ async function runAddColumn(flags) {
|
|
|
871
883
|
return 2;
|
|
872
884
|
return 0;
|
|
873
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
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* dove-sn host-assets:
|
|
993
|
+
* --dir <dist> Required. Path to the pre-built dist/ directory.
|
|
994
|
+
* --app <sys_id> Required. Application record sys_id (m2m `application`).
|
|
995
|
+
* --scope <namespace> Required. Carrier scope, e.g. x_cadso_app_shell.
|
|
996
|
+
* --update-set <sys_id> Optional. Defaults to the scope's current update set.
|
|
997
|
+
* --max-bytes <n> Optional. Per-chunk serve cap (default ~5 MB).
|
|
998
|
+
* --allow-oversize Optional. Warn instead of failing on an oversize chunk.
|
|
999
|
+
* --dry-run Optional. Plan only; no writes/uploads/prunes.
|
|
1000
|
+
* --json Optional. Emit the structured HostAssetsResult.
|
|
1001
|
+
*
|
|
1002
|
+
* Exit codes: 0 done/dry-run, 1 bad args, 2 a write landed but read-back is unverified.
|
|
1003
|
+
*/
|
|
1004
|
+
async function runHostAssets(flags) {
|
|
1005
|
+
var dir = flags.dir;
|
|
1006
|
+
var app = flags.app;
|
|
1007
|
+
var scope = flags.scope;
|
|
1008
|
+
if (!dir || !app || !scope) {
|
|
1009
|
+
process.stderr.write("host-assets: --dir, --app and --scope are required\n");
|
|
1010
|
+
return 1;
|
|
1011
|
+
}
|
|
1012
|
+
var params = { dir: path.resolve(dir), app: app, scope: scope };
|
|
1013
|
+
var us = flags["update-set"] || flags.updateSetSysId;
|
|
1014
|
+
if (us)
|
|
1015
|
+
params.updateSetSysId = us;
|
|
1016
|
+
if (flags["max-bytes"])
|
|
1017
|
+
params.maxBytes = Number(flags["max-bytes"]);
|
|
1018
|
+
if (flags["allow-oversize"] === "true")
|
|
1019
|
+
params.allowOversize = true;
|
|
1020
|
+
if (flags["dry-run"] === "true")
|
|
1021
|
+
params.dryRun = true;
|
|
1022
|
+
var client = (0, client_1.createClient)({});
|
|
1023
|
+
var result = await (0, hostAssets_1.hostAssets)(client, params);
|
|
1024
|
+
if (flags.json === "true") {
|
|
1025
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
process.stdout.write((0, hostAssets_1.formatHostAssetsResult)(result) + "\n");
|
|
1029
|
+
}
|
|
1030
|
+
var unverified = !result.dryRun && result.chunks.some(function (c) { return !c.verified; });
|
|
1031
|
+
return unverified ? 2 : 0;
|
|
1032
|
+
}
|
|
874
1033
|
async function main() {
|
|
875
1034
|
var parsed = parseArgs(process.argv.slice(2));
|
|
876
1035
|
// Load credentials before any command runs. `--env`/`--env-file` (or the
|
|
@@ -905,6 +1064,15 @@ async function main() {
|
|
|
905
1064
|
if (parsed.command === "add-column") {
|
|
906
1065
|
return await runAddColumn(parsed.flags);
|
|
907
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
|
+
}
|
|
1073
|
+
if (parsed.command === "host-assets") {
|
|
1074
|
+
return await runHostAssets(parsed.flags);
|
|
1075
|
+
}
|
|
908
1076
|
if (parsed.command === "test-flow") {
|
|
909
1077
|
return await runTestFlow(parsed.flags);
|
|
910
1078
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -34,6 +34,15 @@ export interface TableSchema {
|
|
|
34
34
|
fields: Array<TableSchemaField>;
|
|
35
35
|
primary_key: string;
|
|
36
36
|
}
|
|
37
|
+
/** A sys_attachment row, as returned by the native Attachment API. */
|
|
38
|
+
export interface AttachmentMeta {
|
|
39
|
+
sys_id: string;
|
|
40
|
+
file_name: string;
|
|
41
|
+
content_type: string;
|
|
42
|
+
/** SHA-256 hex of the file content, computed by ServiceNow. Absent on older instances. */
|
|
43
|
+
hash?: string;
|
|
44
|
+
size_bytes?: string;
|
|
45
|
+
}
|
|
37
46
|
export interface ServiceNowClient {
|
|
38
47
|
table: {
|
|
39
48
|
/** GET /api/now/table/<t>?sysparm_query=...&sysparm_limit=N — returns result array. */
|
|
@@ -113,5 +122,31 @@ export interface ServiceNowClient {
|
|
|
113
122
|
/** POST an arbitrary native ServiceNow REST path with a JSON body. See `get`. */
|
|
114
123
|
post: <T = any>(path: string, body: any) => Promise<T>;
|
|
115
124
|
};
|
|
125
|
+
attachment: {
|
|
126
|
+
/**
|
|
127
|
+
* GET /api/now/attachment?sysparm_query=table_name=<t>^table_sys_id=<id> —
|
|
128
|
+
* list the sys_attachment rows on a record. Read-only.
|
|
129
|
+
*/
|
|
130
|
+
listFor: (params: {
|
|
131
|
+
table: string;
|
|
132
|
+
sysId: string;
|
|
133
|
+
}) => Promise<Array<AttachmentMeta>>;
|
|
134
|
+
/**
|
|
135
|
+
* POST /api/now/attachment/file — upload raw bytes as a sys_attachment on a record.
|
|
136
|
+
* The Buffer is sent verbatim with `contentType` as the request Content-Type (binary,
|
|
137
|
+
* not JSON). Reuses the shared auth/retry/throttle transport — not a bespoke HTTP path.
|
|
138
|
+
*/
|
|
139
|
+
upload: (params: {
|
|
140
|
+
table: string;
|
|
141
|
+
sysId: string;
|
|
142
|
+
fileName: string;
|
|
143
|
+
contentType: string;
|
|
144
|
+
data: Buffer;
|
|
145
|
+
}) => Promise<AttachmentMeta>;
|
|
146
|
+
/** DELETE /api/now/attachment/<sysId> — remove a single attachment. */
|
|
147
|
+
remove: (params: {
|
|
148
|
+
sysId: string;
|
|
149
|
+
}) => Promise<void>;
|
|
150
|
+
};
|
|
116
151
|
}
|
|
117
152
|
export declare function createClient(config?: ServiceNowClientConfig): ServiceNowClient;
|
package/dist/client.js
CHANGED
|
@@ -316,6 +316,37 @@ function createClient(config = {}) {
|
|
|
316
316
|
post: function (path, body) {
|
|
317
317
|
return request({ method: "POST", url: path, data: body }, "now.post(" + path + ")");
|
|
318
318
|
}
|
|
319
|
+
},
|
|
320
|
+
attachment: {
|
|
321
|
+
listFor: async function (params) {
|
|
322
|
+
var data = await request({
|
|
323
|
+
method: "GET",
|
|
324
|
+
url: "/api/now/attachment",
|
|
325
|
+
params: {
|
|
326
|
+
sysparm_query: "table_name=" + params.table + "^table_sys_id=" + params.sysId,
|
|
327
|
+
sysparm_fields: "sys_id,file_name,content_type,hash,size_bytes"
|
|
328
|
+
}
|
|
329
|
+
}, "attachment.listFor(" + params.table + ")");
|
|
330
|
+
return (data && data.result) || [];
|
|
331
|
+
},
|
|
332
|
+
upload: async function (params) {
|
|
333
|
+
var data = await request({
|
|
334
|
+
method: "POST",
|
|
335
|
+
url: "/api/now/attachment/file",
|
|
336
|
+
params: {
|
|
337
|
+
table_name: params.table,
|
|
338
|
+
table_sys_id: params.sysId,
|
|
339
|
+
file_name: params.fileName
|
|
340
|
+
},
|
|
341
|
+
data: params.data,
|
|
342
|
+
// Binary upload: override the client's default application/json.
|
|
343
|
+
headers: { "content-type": params.contentType }
|
|
344
|
+
}, "attachment.upload(" + params.fileName + ")");
|
|
345
|
+
return (data && data.result) || data;
|
|
346
|
+
},
|
|
347
|
+
remove: async function (params) {
|
|
348
|
+
await request({ method: "DELETE", url: "/api/now/attachment/" + params.sysId }, "attachment.remove(" + params.sysId + ")");
|
|
349
|
+
}
|
|
319
350
|
}
|
|
320
351
|
};
|
|
321
352
|
}
|
|
@@ -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,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hostAssets — deploy a pre-built front-end bundle to ServiceNow.
|
|
3
|
+
*
|
|
4
|
+
* For each chunk in a built dist/ (index.html + assets/*.{js,css}) this:
|
|
5
|
+
* 1. Upserts a carrier sys_ui_script named `app_shell_asset:<vite-relative-path>`.
|
|
6
|
+
* The name carries the rotating hash on purpose — the Scripted REST serving
|
|
7
|
+
* resource resolves an asset request to its carrier by this exact name, so the
|
|
8
|
+
* verb's naming MUST match the path the built index.html references, or the
|
|
9
|
+
* asset 404s on serve.
|
|
10
|
+
* 2. Stores the chunk's bytes as a sys_attachment on that record (the script field
|
|
11
|
+
* caps at 65 KB; real chunks are far larger). Identical bytes are detected by
|
|
12
|
+
* SHA-256 and left in place.
|
|
13
|
+
* 3. Wires an x_cadso_app_shell_m2m_app_script row (application, script, chunk_role,
|
|
14
|
+
* order) so the app shell loads the chunk.
|
|
15
|
+
* 4. Prunes carriers + m2m rows for chunks no longer in the build — hashes rotate
|
|
16
|
+
* every build, so last build's records would otherwise pile up.
|
|
17
|
+
*
|
|
18
|
+
* The sys_ui_script + m2m writes route through the Dovetail Scripted REST API and are
|
|
19
|
+
* captured in the target update set. Attachments are data, not customizations, so they
|
|
20
|
+
* are not part of the update set. Re-runnable per build, per instance; idempotent for
|
|
21
|
+
* an identical dist/.
|
|
22
|
+
*
|
|
23
|
+
* The serving layer streams each chunk via GlideSysAttachment.getContentStream(), capped
|
|
24
|
+
* at ~5 MB (glide.scriptable.excel.max_file_size). A chunk at/over the cap truncates on
|
|
25
|
+
* serve, so the verb fails fast on an oversize chunk unless allowOversize is set.
|
|
26
|
+
*/
|
|
27
|
+
import type { ServiceNowClient } from "./client";
|
|
28
|
+
import type { ChunkInfo, HostAssetsParams, HostAssetsResult } from "./types";
|
|
29
|
+
/**
|
|
30
|
+
* Classify build files into ordered ChunkInfo. Pure — no filesystem access — so the
|
|
31
|
+
* naming + role + order logic is unit-testable in isolation.
|
|
32
|
+
*/
|
|
33
|
+
export declare function classifyChunks(files: Array<{
|
|
34
|
+
viteRelPath: string;
|
|
35
|
+
ext: string;
|
|
36
|
+
isIndexHtml: boolean;
|
|
37
|
+
}>): Array<ChunkInfo>;
|
|
38
|
+
/**
|
|
39
|
+
* Deploy a built dist/ to ServiceNow as app-shell carrier scripts + attachments + m2m
|
|
40
|
+
* wiring. Idempotent; re-running an identical dist/ reports every chunk unchanged.
|
|
41
|
+
*/
|
|
42
|
+
export declare function hostAssets(client: ServiceNowClient, params: HostAssetsParams): Promise<HostAssetsResult>;
|
|
43
|
+
/** Human-readable one-line-per-chunk summary for the CLI. */
|
|
44
|
+
export declare function formatHostAssetsResult(r: HostAssetsResult): string;
|