@tenonhq/dovetail-servicenow 0.0.21 → 0.0.23

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,7 @@ 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 hostAssets_1 = require("./hostAssets");
82
83
  const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
83
84
  function parseArgs(argv) {
84
85
  var command = argv[0] || "";
@@ -706,6 +707,13 @@ function printHelp() {
706
707
  " --columns \"Label:type:max, ...\" OR --from-json <spec.json>\n" +
707
708
  " [--extends <t>] [--number-prefix <p>] [--user-role <r>]\n" +
708
709
  " [--no-acls] [--no-menu] [--update-set <sys_id>] [--dry-run] [--json])\n" +
710
+ " add-column Add ONE column to an EXISTING table via the Studio form save, then verify\n" +
711
+ " (--table <name|sys_id> --label <l> --type <t>\n" +
712
+ " [--name <element>] [--max-length <n>] [--reference <table>]\n" +
713
+ " [--scope <s>] [--update-set <sys_id>] [--dry-run] [--json])\n" +
714
+ " host-assets Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)\n" +
715
+ " (--dir <dist> --app <sys_id> --scope <namespace>\n" +
716
+ " [--update-set <sys_id>] [--max-bytes <n>] [--allow-oversize] [--dry-run] [--json])\n" +
709
717
  " test-flow Validate (default) or run a flow/subflow\n" +
710
718
  " (--sys-id <sys_id> [--execute --confirm] [--inputs <json>] [--json])\n" +
711
719
  " edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
@@ -806,6 +814,109 @@ async function runCreateTable(flags) {
806
814
  return 2;
807
815
  return 0;
808
816
  }
817
+ /**
818
+ * dove-sn add-column:
819
+ * --table x_cadso_journey --label URL --type url
820
+ * [--name url] [--max-length 1024] [--reference <table>]
821
+ * [--scope x_cadso_journey] [--update-set <sys_id>] [--save-action <sys_id>]
822
+ * [--columns-rel-id <sys_id>] [--from-json <spec.json>] [--dry-run] [--debug] [--json]
823
+ */
824
+ async function runAddColumn(flags) {
825
+ var spec = {};
826
+ if (flags["from-json"]) {
827
+ spec = JSON.parse(fs.readFileSync(path.resolve(flags["from-json"]), "utf8"));
828
+ }
829
+ var table = flags.table || spec.table;
830
+ var column = spec.column;
831
+ if (flags.label || flags.type) {
832
+ column = { label: flags.label || "", type: flags.type || "string" };
833
+ if (flags.name)
834
+ column.name = flags.name;
835
+ if (flags["max-length"])
836
+ column.max_length = flags["max-length"];
837
+ if (flags.reference)
838
+ column.reference = flags.reference;
839
+ }
840
+ if (!table || !column || !column.label) {
841
+ process.stderr.write("add-column: --table and --label (with --type) are required (or --from-json)\n");
842
+ return 1;
843
+ }
844
+ var params = {
845
+ client: (0, client_1.createClient)({}),
846
+ table: table,
847
+ column: column
848
+ };
849
+ var scope = flags.scope || spec.scope;
850
+ if (scope)
851
+ params.scope = scope;
852
+ var us = flags["update-set"] || spec.updateSetSysId;
853
+ if (us)
854
+ params.updateSetSysId = us;
855
+ var sa = flags["save-action"] || spec.saveActionSysId;
856
+ if (sa)
857
+ params.saveActionSysId = sa;
858
+ var relId = flags["columns-rel-id"] || spec.columnsRelId;
859
+ if (relId)
860
+ params.columnsRelId = relId;
861
+ if (flags["dry-run"] === "true" || spec.dryRun === true)
862
+ params.dryRun = true;
863
+ if (flags.debug === "true" || spec.debug === true)
864
+ params.debug = true;
865
+ var result = await (0, table_1.addColumn)(params);
866
+ if (flags.json === "true") {
867
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
868
+ }
869
+ else {
870
+ process.stdout.write("[" + result.status + "] " + result.table + "." + result.element + " (" + result.internalType + ")"
871
+ + (result.verified ? " — verified" : "")
872
+ + "\n" + result.note + "\n");
873
+ }
874
+ if (result.status === "failed")
875
+ return 2;
876
+ return 0;
877
+ }
878
+ /**
879
+ * dove-sn host-assets:
880
+ * --dir <dist> Required. Path to the pre-built dist/ directory.
881
+ * --app <sys_id> Required. Application record sys_id (m2m `application`).
882
+ * --scope <namespace> Required. Carrier scope, e.g. x_cadso_app_shell.
883
+ * --update-set <sys_id> Optional. Defaults to the scope's current update set.
884
+ * --max-bytes <n> Optional. Per-chunk serve cap (default ~5 MB).
885
+ * --allow-oversize Optional. Warn instead of failing on an oversize chunk.
886
+ * --dry-run Optional. Plan only; no writes/uploads/prunes.
887
+ * --json Optional. Emit the structured HostAssetsResult.
888
+ *
889
+ * Exit codes: 0 done/dry-run, 1 bad args, 2 a write landed but read-back is unverified.
890
+ */
891
+ async function runHostAssets(flags) {
892
+ var dir = flags.dir;
893
+ var app = flags.app;
894
+ var scope = flags.scope;
895
+ if (!dir || !app || !scope) {
896
+ process.stderr.write("host-assets: --dir, --app and --scope are required\n");
897
+ return 1;
898
+ }
899
+ var params = { dir: path.resolve(dir), app: app, scope: scope };
900
+ var us = flags["update-set"] || flags.updateSetSysId;
901
+ if (us)
902
+ params.updateSetSysId = us;
903
+ if (flags["max-bytes"])
904
+ params.maxBytes = Number(flags["max-bytes"]);
905
+ if (flags["allow-oversize"] === "true")
906
+ params.allowOversize = true;
907
+ if (flags["dry-run"] === "true")
908
+ params.dryRun = true;
909
+ var client = (0, client_1.createClient)({});
910
+ var result = await (0, hostAssets_1.hostAssets)(client, params);
911
+ if (flags.json === "true") {
912
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
913
+ }
914
+ else {
915
+ process.stdout.write((0, hostAssets_1.formatHostAssetsResult)(result) + "\n");
916
+ }
917
+ var unverified = !result.dryRun && result.chunks.some(function (c) { return !c.verified; });
918
+ return unverified ? 2 : 0;
919
+ }
809
920
  async function main() {
810
921
  var parsed = parseArgs(process.argv.slice(2));
811
922
  // Load credentials before any command runs. `--env`/`--env-file` (or the
@@ -837,6 +948,12 @@ async function main() {
837
948
  if (parsed.command === "create-table") {
838
949
  return await runCreateTable(parsed.flags);
839
950
  }
951
+ if (parsed.command === "add-column") {
952
+ return await runAddColumn(parsed.flags);
953
+ }
954
+ if (parsed.command === "host-assets") {
955
+ return await runHostAssets(parsed.flags);
956
+ }
840
957
  if (parsed.command === "test-flow") {
841
958
  return await runTestFlow(parsed.flags);
842
959
  }
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,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;