@tenonhq/dovetail-servicenow 0.0.50 → 0.0.52
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 +120 -42
- package/dist/cli.js +115 -17
- package/dist/cliUsage.js +37 -9
- package/dist/createRecord.d.ts +2 -1
- package/dist/createRecord.js +12 -1
- package/dist/deleteRecord.d.ts +35 -12
- package/dist/deleteRecord.js +238 -38
- package/dist/index.d.ts +3 -1
- package/dist/index.js +15 -4
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +121 -35
- package/dist/mcp/schemas.d.ts +22 -0
- package/dist/mcp/schemas.js +25 -6
- package/dist/table/addColumn.d.ts +3 -2
- package/dist/table/addColumn.js +63 -35
- package/dist/table/addIndex.d.ts +7 -1
- package/dist/table/addIndex.js +56 -4
- package/dist/table/createIndex.d.ts +119 -50
- package/dist/table/createIndex.js +750 -137
- package/dist/table/designAccess.d.ts +2 -2
- package/dist/table/designAccess.js +30 -7
- package/dist/table/index.d.ts +1 -1
- package/dist/table/index.js +10 -4
- package/dist/table/listIndexes.d.ts +40 -0
- package/dist/table/listIndexes.js +159 -24
- package/dist/table/overrideColumn.d.ts +12 -18
- package/dist/table/overrideColumn.js +13 -41
- package/dist/table/setColumn.d.ts +5 -3
- package/dist/table/setColumn.js +27 -8
- package/dist/table/updateSetGuard.d.ts +23 -0
- package/dist/table/updateSetGuard.js +57 -0
- package/dist/uxEvents.d.ts +114 -0
- package/dist/uxEvents.js +407 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -736,60 +736,102 @@ admin, and `sys_index_column` does not exist at all (HTTP 400 `Invalid table`),
|
|
|
736
736
|
is no two-table index model to join and nothing to cross-check against.
|
|
737
737
|
|
|
738
738
|
Each row comes back as `{ name, columns, type, rawColumns }`. `columns` is the view's
|
|
739
|
-
bracketed `column_names` cell (`"[phone]"`, `"[a
|
|
739
|
+
bracketed `column_names` cell (`"[phone]"`, `"[a;b]"` - semicolon-separated when composite) **parsed** into a list - never
|
|
740
740
|
substring-matched, because `"[owner_id]"` contains `"owner"`. `type` is `access_method`.
|
|
741
741
|
|
|
742
742
|
**Uniqueness is not readable.** `v_db_index` has no uniqueness field, so a unique index
|
|
743
743
|
and an ordinary one are indistinguishable in it: `unique` is left **absent** rather than
|
|
744
744
|
guessed, and `uniqueness-enforced` is reported in `unverified` on every result. Only a
|
|
745
|
-
duplicate-insert test proves enforcement. An empty result
|
|
746
|
-
|
|
745
|
+
duplicate-insert test proves enforcement. An empty result for a table that does not exist
|
|
746
|
+
means the table name is wrong - every physical table has a `PRIMARY`.
|
|
747
|
+
|
|
748
|
+
**Table-per-hierarchy children list their storage root's indexes.** `v_db_index` lists
|
|
749
|
+
indexes by *physical* table, and a table stored in an ancestor's physical table (anything
|
|
750
|
+
extending `task`, for example) has no rows under its own name. When the table exists but
|
|
751
|
+
has no rows, its `super_class` chain is walked to the first ancestor that has them, and
|
|
752
|
+
that root's indexes are listed - with `storageTable` and the note naming the root.
|
|
753
|
+
`index-create` and `add-index` **refuse** such a child on the live path, naming the root,
|
|
754
|
+
rather than build on it or report a false "NOT created".
|
|
747
755
|
|
|
748
756
|
### Create an index (composite and non-unique included)
|
|
749
757
|
|
|
750
|
-
> **
|
|
751
|
-
>
|
|
752
|
-
>
|
|
753
|
-
>
|
|
758
|
+
> **An index IS captured in an update set.** The platform's build job writes a
|
|
759
|
+
> `sys_update_xml` row (`type=Indexes`, name `sys_index_<table>_<col>_<col>…`) into the
|
|
760
|
+
> session user's **current** update set - the Database Indexes dialog says so itself, and
|
|
761
|
+
> it was read back live on tenonworkstudio 2026-10-08. `--update-set` is therefore
|
|
762
|
+
> **required** on the live path: the verb pins that set as current first and reads the
|
|
763
|
+
> capture row back from it afterwards. The physical index is still built per instance;
|
|
764
|
+
> committing the set elsewhere rebuilds it there.
|
|
754
765
|
|
|
755
766
|
```bash
|
|
756
|
-
# Dry-run (the DEFAULT) - sends nothing and reads nothing
|
|
767
|
+
# Dry-run (the DEFAULT) - sends nothing and reads nothing; no update set needed
|
|
757
768
|
npx dove-sn index-create --table x_cadso_journey_instance --columns state,created_on
|
|
758
769
|
|
|
759
770
|
# Send it
|
|
760
771
|
npx dove-sn index-create \
|
|
761
|
-
--table x_cadso_journey_instance --columns state,created_on
|
|
772
|
+
--table x_cadso_journey_instance --columns state,created_on \
|
|
773
|
+
--update-set <sys_id> --confirm --json
|
|
762
774
|
```
|
|
763
775
|
|
|
764
776
|
This is what `add-index` cannot do. `sys_dictionary.unique` - the only record-shaped lever
|
|
765
777
|
- is **per-column and unique-only**, so composite and plain indexes have no record path at
|
|
766
|
-
all
|
|
767
|
-
|
|
768
|
-
`
|
|
769
|
-
|
|
770
|
-
|
|
778
|
+
all (`sys_index` is API-level-ACL 403, `sys_index_column` does not exist). `index-create`
|
|
779
|
+
instead replays the two GlideAjax calls the platform's own Database Indexes dialog makes
|
|
780
|
+
on `xmlhttp.do` over a form-login session - taken from a HAR of the Studio dialog plus the
|
|
781
|
+
`dialog_index_create` UI page's client script, both read live:
|
|
782
|
+
|
|
783
|
+
1. `IndexCreatorErrorChecker` / `canCreate` - the dialog's pre-flight (`sysparm_table_name`,
|
|
784
|
+
`sysparm_field_names`, `sysparm_unique`, `sysparm_access_method`). A `canCreate:false`
|
|
785
|
+
verdict is returned verbatim with its `errorCode` and nothing is scheduled.
|
|
786
|
+
2. `ScheduleCreator` / `createSchedule` - schedules the build (`sysparm_table`,
|
|
787
|
+
`sysparm_fields`, `sysparm_access_method` - default `btree`, exactly as the dialog's
|
|
788
|
+
client JS does - `sysparm_unique` `true|false`, empty `sysparm_email` = no
|
|
789
|
+
notification, empty `sysparm_schedule_name`).
|
|
790
|
+
|
|
791
|
+
(The `index_creator_dialog` page's `sys_action=create_index` form is inert - the browser
|
|
792
|
+
never submits it, and the UI page has no processing script. The earlier form-POST replay
|
|
793
|
+
was a silent no-op for that reason.)
|
|
771
794
|
|
|
772
795
|
- **Dry-run by default.** Without `--confirm` nothing is sent *and nothing is read*;
|
|
773
796
|
`--dry-run` forces a plan even with `--confirm`.
|
|
797
|
+
- **Target checks before anything else.** The table must exist; the update set must exist,
|
|
798
|
+
be `in progress`, and belong to the table's application scope - a set in another scope
|
|
799
|
+
is refused, because the capture row would land in the wrong scope.
|
|
774
800
|
- **Idempotent.** On the live path `v_db_index` is read first, and an index over *exactly*
|
|
775
|
-
these columns short-circuits to `already-exists` with no form session and no
|
|
776
|
-
Column **order** is part of an index's identity - `[a
|
|
777
|
-
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
- **
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
801
|
+
these columns short-circuits to `already-exists` with no pin, no form session and no
|
|
802
|
+
write. Column **order** is part of an index's identity - `[a;b]` is not `[b;a]`.
|
|
803
|
+
- **Table-per-hierarchy children are refused.** A table with no `v_db_index` rows of its
|
|
804
|
+
own whose ancestor has them is stored in that ancestor's physical table; the run stops
|
|
805
|
+
before any write and names the root (run against the root if that is what you want).
|
|
806
|
+
- **Identity check (warns, never refuses).** The pin runs through the REST client (an API
|
|
807
|
+
key when one is configured) but the build is scheduled by the form session (always
|
|
808
|
+
`SN_USER`), and the build captures into the *form* user's current set. The REST caller's
|
|
809
|
+
own `sys_user` row is read first; when it is not the form-login user (or cannot be read)
|
|
810
|
+
the run still goes ahead and every result note carries an `IDENTITY WARNING`. The capture
|
|
811
|
+
read-back then reports where the row actually landed (`captured` / `captureFoundIn`).
|
|
812
|
+
- **The pin is read back.** The set is pinned with Dovetail's own `changeUpdateSet` and
|
|
813
|
+
`currentUpdateSet` is read; a pin that did not take stops the run before the session
|
|
814
|
+
opens.
|
|
815
|
+
- **`--name` is refused.** The dialog has no name input; ServiceNow names the index after
|
|
816
|
+
its leading column (live: `[sys_created_on;status;version_step;version]` → index
|
|
817
|
+
`sys_created_on`). The created index's *real* name is returned in `name`.
|
|
818
|
+
- **The read-back is the proof.** After scheduling, the index is polled for in `v_db_index`
|
|
819
|
+
(default 10 checks, 3 s apart). If it never appears the status is `failed`: an accepted
|
|
820
|
+
schedule is not evidence an ALTER ran, and a unique index cannot build over duplicate
|
|
821
|
+
values (EMPTY counts). Then the capture row is looked for in the pinned set:
|
|
822
|
+
`captured:true` only when it was read back; an index that exists but was not captured
|
|
823
|
+
is `created:true, captured:false` with `update-set-capture` in `unverified` - and
|
|
824
|
+
exit code 2, because it will not travel. In that case the capture name is searched
|
|
825
|
+
across every set and the set(s) it actually landed in come back in `captureFoundIn`.
|
|
784
826
|
- **Uniqueness is still never claimed.** `uniqueness-enforced` stays in `unverified` on
|
|
785
827
|
every status.
|
|
786
828
|
|
|
787
|
-
**Requires a username+password identity that can form-log-in.**
|
|
788
|
-
API-key-only
|
|
789
|
-
|
|
790
|
-
replay (including `create-table`'s) can work in that state.
|
|
829
|
+
**Requires a username+password identity that can form-log-in.** `xmlhttp.do` ignores
|
|
830
|
+
Basic auth and API keys, so an API-key-only, SSO or MFA identity fails at the session with
|
|
831
|
+
that diagnosis (TenonHQ/Dovetail#292).
|
|
791
832
|
|
|
792
|
-
Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed
|
|
833
|
+
Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed - and `2`
|
|
834
|
+
when the index was created but its capture row was not found in the pinned set.
|
|
793
835
|
|
|
794
836
|
### Set a field on a record
|
|
795
837
|
|
|
@@ -835,7 +877,8 @@ npx dove-sn create-record \
|
|
|
835
877
|
switches the executing user's app scope + update set server-side, inserts, and
|
|
836
878
|
restores both — so the record is owned by the right app and the insert is captured
|
|
837
879
|
in the right update set. Like `set-field` it **refuses** schema tables and verifies
|
|
838
|
-
via read-back.
|
|
880
|
+
via read-back. It also **refuses** `sys_update_set` — the op cannot set an update
|
|
881
|
+
set's application, so create sets with `dove createUpdateSet` instead. `--scope` and `--update-set` are required; `--if-absent
|
|
839
882
|
"<encoded-query>"` makes re-runs idempotent (the insert is skipped when the query
|
|
840
883
|
already matches a row). Exit codes: `0` created / skipped-in-sync / dry-run, `1` bad
|
|
841
884
|
args, `2` write landed unverified (or skipped with drift). To **update** an existing
|
|
@@ -892,14 +935,19 @@ npx dove-sn delete-record \
|
|
|
892
935
|
`delete-record` wraps the core `deleteRecord` op. It is **dry-run by default** —
|
|
893
936
|
nothing is deleted without `--apply` (`--dry-run` wins if both are given). `--sys-id`
|
|
894
937
|
must be a 32-character lowercase hex id and `--table` a plain table name; both are
|
|
895
|
-
validated before any network call. `--update-set` is **required
|
|
896
|
-
|
|
897
|
-
[#297](https://github.com/TenonHQ/Dovetail/issues/297) ships server-side
|
|
898
|
-
`update_set_sys_id` and captures the delete into the session's **current** update set
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
`
|
|
902
|
-
`
|
|
938
|
+
validated before any network call. `--update-set` is **required**, must exist and be
|
|
939
|
+
**in progress** (checked on the dry-run too). Until
|
|
940
|
+
[#297](https://github.com/TenonHQ/Dovetail/issues/297) ships server-side the op ignores
|
|
941
|
+
`update_set_sys_id` and captures the delete into the session's **current** update set, so
|
|
942
|
+
the verb pins `--update-set` as current first (refusing, nothing deleted, if the pin does
|
|
943
|
+
not read back) and then reads the DELETE row back from `sys_update_xml`. The result carries
|
|
944
|
+
`captured`, `capturedInto` and `captureState` (`in-set` / `other-set` / `none` /
|
|
945
|
+
`unverified`). Like its siblings it **refuses** schema tables (`sys_db_object` /
|
|
946
|
+
`sys_dictionary`). A failed delete call never skips the read-back. Exit codes: `0` deleted
|
|
947
|
+
and captured in the set, a table that writes no capture at all (`none`, with a note), or a
|
|
948
|
+
dry-run; `1` bad args, no such record, or an unknown/closed update set; `2` the record is
|
|
949
|
+
**still present** on read-back (including a server-refused delete), its state is unknown,
|
|
950
|
+
the pin did not take, or the DELETE landed in a different set / could not be read back.
|
|
903
951
|
|
|
904
952
|
All three verbs are exported for programmatic use:
|
|
905
953
|
|
|
@@ -917,6 +965,36 @@ var r = await setField({
|
|
|
917
965
|
console.log(r.status, r.verified); // "applied" true
|
|
918
966
|
```
|
|
919
967
|
|
|
968
|
+
### Register UI component events (sync-ux-events)
|
|
969
|
+
|
|
970
|
+
UI Builder can only map an event a component dispatches when it exists as a
|
|
971
|
+
`sys_ux_event` record **and** is listed in the component macroponent's
|
|
972
|
+
`dispatched_events`. A component deploy does not reliably create either, so
|
|
973
|
+
`sync-ux-events` reads the component's `now-ui.json` and reconciles every
|
|
974
|
+
`components.<tag>.actions[]` entry against the instance.
|
|
975
|
+
|
|
976
|
+
```bash
|
|
977
|
+
# Dry-run (the default) — per action: ok / create / link / drift / ambiguous, plus orphans
|
|
978
|
+
npx dove-sn sync-ux-events --file path/to/now-ui.json
|
|
979
|
+
|
|
980
|
+
# Apply — creates the missing events and appends them to dispatched_events
|
|
981
|
+
npx dove-sn sync-ux-events --file path/to/now-ui.json \
|
|
982
|
+
--component cadso-journey-builder --update-set <sys_id> --apply
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
The macroponent is the `sys_ux_macroponent` (category `component`) whose
|
|
986
|
+
`root_component` is the `sys_ux_lib_component` with that tag; events are created in
|
|
987
|
+
the macroponent's scope via the scope-aware `createRecord` op and the list edit goes
|
|
988
|
+
through `pushWithUpdateSet`, both captured into `--update-set`. The write is
|
|
989
|
+
**append-only** — existing `dispatched_events` entries are never dropped or reordered —
|
|
990
|
+
and is re-read to verify. Label/description **drift** and **orphans** (linked but no
|
|
991
|
+
longer declared) are reported, never changed; an event name carried by several records
|
|
992
|
+
is **ambiguous** and never written. Event names and tags are charset-validated before
|
|
993
|
+
they reach a query. Re-runs are no-ops. Exit codes: `0` in sync / dry-run / applied and
|
|
994
|
+
verified; `1` bad args or unreadable file; `2` an unresolved or ambiguous
|
|
995
|
+
component/event, or a write that did not verify. MCP: `sync_ux_events` (dry-run unless
|
|
996
|
+
`confirm:true`).
|
|
997
|
+
|
|
920
998
|
### Invoke an arbitrary REST operation
|
|
921
999
|
|
|
922
1000
|
Invoke any authenticated ServiceNow REST operation — an application's own
|
|
@@ -1167,15 +1245,15 @@ read back from the `v_db_index` view - uniqueness enforcement is always reported
|
|
|
1167
1245
|
unverified) / `index_list` (read-only: a table's database indexes from `v_db_index`,
|
|
1168
1246
|
the only index read surface - `sys_index` is API-level-ACL 403 and `sys_index_column`
|
|
1169
1247
|
does not exist) / `index_create` (create an index, composite and non-unique included, by
|
|
1170
|
-
replaying the
|
|
1171
|
-
|
|
1172
|
-
|
|
1248
|
+
replaying the Database Indexes dialog's own processor calls; dry-run by default, idempotent,
|
|
1249
|
+
pinned to a required update set, read back from `v_db_index` and the capture row read back
|
|
1250
|
+
from `sys_update_xml`), the record-write verbs `set_field` (update scalar fields on an
|
|
1173
1251
|
existing record), `create_record` (insert one record) and `delete_record` (delete one
|
|
1174
1252
|
record — dry-run by default, `confirm:true` to apply, `updateSetSysId` required, the
|
|
1175
1253
|
record read back before AND after so success is only reported once it is confirmed
|
|
1176
1254
|
gone) — all read-back-verified; `set_field` / `create_record` are captured in the
|
|
1177
|
-
update set you pass, while `delete_record`
|
|
1178
|
-
until [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships — `host_assets` (deploy a built
|
|
1255
|
+
update set you pass, while `delete_record` pins the set as current and reads the DELETE
|
|
1256
|
+
capture back (`captureState`) until [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships — `host_assets` (deploy a built
|
|
1179
1257
|
dist/), plus the Flow Designer
|
|
1180
1258
|
tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
|
|
1181
1259
|
type's model), `action_edit` (structurally edit a published action type — per-step
|
package/dist/cli.js
CHANGED
|
@@ -86,6 +86,7 @@ const deleteRecord_1 = require("./deleteRecord");
|
|
|
86
86
|
const invokeRest_1 = require("./invokeRest");
|
|
87
87
|
const publishApp_1 = require("./publishApp");
|
|
88
88
|
const hostAssets_1 = require("./hostAssets");
|
|
89
|
+
const uxEvents_1 = require("./uxEvents");
|
|
89
90
|
const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
|
|
90
91
|
function parseArgs(argv) {
|
|
91
92
|
// `dove-sn --help` has no verb: a leading flag means an empty command and the
|
|
@@ -1572,7 +1573,14 @@ async function runIndexList(flags) {
|
|
|
1572
1573
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1573
1574
|
return 0;
|
|
1574
1575
|
}
|
|
1575
|
-
process.stdout.write(result.table +
|
|
1576
|
+
process.stdout.write(result.table +
|
|
1577
|
+
" — " +
|
|
1578
|
+
result.indexes.length +
|
|
1579
|
+
" index(es)" +
|
|
1580
|
+
(result.storageTable && result.storageTable !== result.table
|
|
1581
|
+
? " (stored in " + result.storageTable + "'s physical table — these are its indexes)"
|
|
1582
|
+
: "") +
|
|
1583
|
+
"\n");
|
|
1576
1584
|
for (var i = 0; i < result.indexes.length; i += 1) {
|
|
1577
1585
|
var idx = result.indexes[i];
|
|
1578
1586
|
process.stdout.write(" " +
|
|
@@ -1588,16 +1596,19 @@ async function runIndexList(flags) {
|
|
|
1588
1596
|
}
|
|
1589
1597
|
/**
|
|
1590
1598
|
* dove-sn index-create:
|
|
1591
|
-
* --table x_cadso_core_u_smoke --columns a,b
|
|
1592
|
-
* [--confirm] [--dry-run] [--poll-attempts <n>]
|
|
1593
|
-
* [--debug] [--json]
|
|
1599
|
+
* --table x_cadso_core_u_smoke --columns a,b --update-set <sys_id> [--unique]
|
|
1600
|
+
* [--access-method <m>] [--confirm] [--dry-run] [--poll-attempts <n>]
|
|
1601
|
+
* [--poll-interval-ms <n>] [--debug] [--json]
|
|
1594
1602
|
*
|
|
1595
1603
|
* DRY-RUN BY DEFAULT — nothing is sent (and nothing is even READ) without --confirm;
|
|
1596
|
-
* --dry-run forces a plan even with it.
|
|
1597
|
-
*
|
|
1604
|
+
* --dry-run forces a plan even with it. --update-set is REQUIRED on the live path:
|
|
1605
|
+
* the build job captures the index definition (sys_update_xml type=Indexes) into the
|
|
1606
|
+
* user's CURRENT update set, so the verb pins that set first and reads the capture
|
|
1607
|
+
* row back from it afterwards.
|
|
1598
1608
|
*
|
|
1599
1609
|
* Exit codes: 0 created / already-exists / dry-run, 1 bad args, 2 failed (which
|
|
1600
|
-
* includes "
|
|
1610
|
+
* includes "scheduled but no index was read back") — and 2 when the index was read
|
|
1611
|
+
* back but its capture row was NOT found in the pinned set.
|
|
1601
1612
|
*/
|
|
1602
1613
|
async function runIndexCreate(flags) {
|
|
1603
1614
|
var columns = splitList(flags.columns || "");
|
|
@@ -1605,10 +1616,12 @@ async function runIndexCreate(flags) {
|
|
|
1605
1616
|
process.stderr.write("index-create: --table <name> and --columns <a[,b,...]> are required\n");
|
|
1606
1617
|
return 1;
|
|
1607
1618
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1619
|
+
// DRY-RUN BY DEFAULT: --confirm is what sends; --dry-run forces a plan even with it.
|
|
1620
|
+
var indexDryRun = flags["dry-run"] === "true" || flags.confirm !== "true";
|
|
1621
|
+
if (!indexDryRun && !flags["update-set"]) {
|
|
1622
|
+
process.stderr.write("index-create: --update-set is required on the live path — the index " +
|
|
1623
|
+
"definition is captured into the user's CURRENT update set, so it must be " +
|
|
1624
|
+
"pinned first (only a dry-run works without one)\n");
|
|
1612
1625
|
return 1;
|
|
1613
1626
|
}
|
|
1614
1627
|
var params = {
|
|
@@ -1623,8 +1636,8 @@ async function runIndexCreate(flags) {
|
|
|
1623
1636
|
params.name = flags.name;
|
|
1624
1637
|
if (flags["access-method"])
|
|
1625
1638
|
params.accessMethod = flags["access-method"];
|
|
1626
|
-
if (flags["
|
|
1627
|
-
params.
|
|
1639
|
+
if (flags["update-set"])
|
|
1640
|
+
params.updateSetSysId = flags["update-set"];
|
|
1628
1641
|
if (flags.debug === "true")
|
|
1629
1642
|
params.debug = true;
|
|
1630
1643
|
// A non-integer poll setting would make the bounded wait unbounded (or zero).
|
|
@@ -1663,6 +1676,12 @@ async function runIndexCreate(flags) {
|
|
|
1663
1676
|
(result.name ? " -> " + result.name : "") +
|
|
1664
1677
|
(result.instance ? " on " + result.instance : "") +
|
|
1665
1678
|
(result.verified ? " — verified" : "") +
|
|
1679
|
+
(result.updateSet
|
|
1680
|
+
? " [update set " +
|
|
1681
|
+
(result.updateSet.name || result.updateSet.sysId) +
|
|
1682
|
+
(result.captured ? " — captured" : " — capture NOT verified") +
|
|
1683
|
+
"]"
|
|
1684
|
+
: "") +
|
|
1666
1685
|
"\n" +
|
|
1667
1686
|
result.note +
|
|
1668
1687
|
"\nUNVERIFIED: " +
|
|
@@ -1671,6 +1690,10 @@ async function runIndexCreate(flags) {
|
|
|
1671
1690
|
}
|
|
1672
1691
|
if (result.status === "failed")
|
|
1673
1692
|
return 2;
|
|
1693
|
+
// An index that exists but whose definition was not captured will not travel —
|
|
1694
|
+
// surface that as a non-zero exit, the same way an unread-back index is.
|
|
1695
|
+
if (result.status === "created" && !result.captured)
|
|
1696
|
+
return 2;
|
|
1674
1697
|
return 0;
|
|
1675
1698
|
}
|
|
1676
1699
|
/** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
|
|
@@ -1991,18 +2014,76 @@ async function runCreateRecord(flags) {
|
|
|
1991
2014
|
return 2;
|
|
1992
2015
|
return 0;
|
|
1993
2016
|
}
|
|
2017
|
+
/**
|
|
2018
|
+
* dove-sn sync-ux-events:
|
|
2019
|
+
* --file <path/to/now-ui.json>
|
|
2020
|
+
* [--component <tag>]
|
|
2021
|
+
* [--update-set <sys_id>] (required with --apply)
|
|
2022
|
+
* [--apply] [--dry-run] [--json]
|
|
2023
|
+
* DRY-RUN BY DEFAULT. Exit codes: 0 in sync / dry-run / applied+verified, 1 bad args or
|
|
2024
|
+
* unreadable file, 2 unresolved/ambiguous component or event, or an unverified write.
|
|
2025
|
+
*/
|
|
2026
|
+
async function runSyncUxEvents(flags, bare) {
|
|
2027
|
+
var bareErr = bareStringFlagError("sync-ux-events", bare);
|
|
2028
|
+
if (bareErr) {
|
|
2029
|
+
process.stderr.write(bareErr);
|
|
2030
|
+
return 1;
|
|
2031
|
+
}
|
|
2032
|
+
var file = flags.file;
|
|
2033
|
+
if (!file) {
|
|
2034
|
+
process.stderr.write("sync-ux-events: --file <now-ui.json> is required\n");
|
|
2035
|
+
return 1;
|
|
2036
|
+
}
|
|
2037
|
+
var apply = flags.apply === "true" && flags["dry-run"] !== "true";
|
|
2038
|
+
var nowUi;
|
|
2039
|
+
try {
|
|
2040
|
+
nowUi = JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
|
|
2041
|
+
}
|
|
2042
|
+
catch (err) {
|
|
2043
|
+
process.stderr.write("sync-ux-events: cannot read " + file + ": " + (err instanceof Error ? err.message : String(err)) + "\n");
|
|
2044
|
+
return 1;
|
|
2045
|
+
}
|
|
2046
|
+
var result;
|
|
2047
|
+
try {
|
|
2048
|
+
result = await (0, uxEvents_1.syncUxEvents)({
|
|
2049
|
+
client: (0, client_1.createClient)({}),
|
|
2050
|
+
nowUi: nowUi,
|
|
2051
|
+
component: flags.component,
|
|
2052
|
+
updateSetSysId: flags["update-set"],
|
|
2053
|
+
apply: apply,
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
catch (err) {
|
|
2057
|
+
var message = err instanceof Error ? err.message : String(err);
|
|
2058
|
+
if (message.indexOf("ux-events: ") === 0)
|
|
2059
|
+
message = message.slice("ux-events: ".length);
|
|
2060
|
+
process.stderr.write("sync-ux-events: " + message + "\n");
|
|
2061
|
+
return 1;
|
|
2062
|
+
}
|
|
2063
|
+
if (flags.json === "true") {
|
|
2064
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2065
|
+
}
|
|
2066
|
+
else {
|
|
2067
|
+
process.stdout.write((0, uxEvents_1.formatUxEventSync)(result) + "\n");
|
|
2068
|
+
}
|
|
2069
|
+
return result.ok ? 0 : 2;
|
|
2070
|
+
}
|
|
1994
2071
|
/**
|
|
1995
2072
|
* dove-sn delete-record:
|
|
1996
2073
|
* --table x_cadso_core_metric_point_type
|
|
1997
2074
|
* --sys-id <32-hex sys_id> (the record to delete)
|
|
1998
|
-
* --update-set <sys_id> (required —
|
|
1999
|
-
* session
|
|
2075
|
+
* --update-set <sys_id> (required — must exist and be in progress; pinned as
|
|
2076
|
+
* the session's current set before the delete, and the
|
|
2077
|
+
* DELETE capture row is read back from sys_update_xml)
|
|
2000
2078
|
* [--apply] (DRY-RUN BY DEFAULT — nothing is deleted without it)
|
|
2001
2079
|
* [--dry-run] [--json] (--dry-run wins over --apply)
|
|
2002
2080
|
* Reads the record BEFORE (a missing record is an error, not a no-op delete) and AFTER
|
|
2003
2081
|
* (success is only reported once the record is confirmed gone).
|
|
2004
|
-
* Exit codes: 0 deleted/dry-run, 1 bad args
|
|
2005
|
-
* record is STILL PRESENT on read-back
|
|
2082
|
+
* Exit codes: 0 deleted (and captured in the requested set) / dry-run, 1 bad args, missing
|
|
2083
|
+
* record or unknown/closed update set, 2 the record is STILL PRESENT on read-back (including a
|
|
2084
|
+
* delete the server refused with an error), its state is unknown, the update-set pin did not
|
|
2085
|
+
* take (nothing deleted), or it was deleted but the DELETE capture landed in a different set or
|
|
2086
|
+
* could not be read back. A table with no update-set capture at all exits 0 with a note.
|
|
2006
2087
|
*/
|
|
2007
2088
|
async function runDeleteRecord(flags) {
|
|
2008
2089
|
var table = flags.table;
|
|
@@ -2048,7 +2129,15 @@ async function runDeleteRecord(flags) {
|
|
|
2048
2129
|
result.sysId +
|
|
2049
2130
|
" → update set " +
|
|
2050
2131
|
result.updateSetSysId +
|
|
2132
|
+
(result.updateSetName ? " (" + result.updateSetName + ")" : "") +
|
|
2051
2133
|
(result.verified ? " — verified gone" : "") +
|
|
2134
|
+
(result.status === "deleted"
|
|
2135
|
+
? (result.captureState === "in-set"
|
|
2136
|
+
? ", capture verified"
|
|
2137
|
+
: result.captureState === "none"
|
|
2138
|
+
? ", no update-set capture (table not recorded?)"
|
|
2139
|
+
: ", NOT captured in that set")
|
|
2140
|
+
: "") +
|
|
2052
2141
|
"\n" +
|
|
2053
2142
|
result.note +
|
|
2054
2143
|
"\n");
|
|
@@ -2058,6 +2147,12 @@ async function runDeleteRecord(flags) {
|
|
|
2058
2147
|
}
|
|
2059
2148
|
if (result.status === "failed")
|
|
2060
2149
|
return 2;
|
|
2150
|
+
// Gone but the DELETE landed in ANOTHER set (the requested set would promote without it),
|
|
2151
|
+
// or the capture could not be read back: neither may read as success to a script. A table
|
|
2152
|
+
// that is not recorded in update sets at all ("none") is a plain data delete — exit 0, and
|
|
2153
|
+
// the note says the delete will not travel.
|
|
2154
|
+
if (result.status === "deleted" && (result.captureState === "other-set" || result.captureState === "unverified"))
|
|
2155
|
+
return 2;
|
|
2061
2156
|
return 0;
|
|
2062
2157
|
}
|
|
2063
2158
|
/**
|
|
@@ -2689,6 +2784,9 @@ async function dispatch(parsed) {
|
|
|
2689
2784
|
if (parsed.command === "create-record") {
|
|
2690
2785
|
return await runCreateRecord(parsed.flags);
|
|
2691
2786
|
}
|
|
2787
|
+
if (parsed.command === "sync-ux-events") {
|
|
2788
|
+
return await runSyncUxEvents(parsed.flags, parsed.bare);
|
|
2789
|
+
}
|
|
2692
2790
|
if (parsed.command === "delete-record") {
|
|
2693
2791
|
return await runDeleteRecord(parsed.flags);
|
|
2694
2792
|
}
|
package/dist/cliUsage.js
CHANGED
|
@@ -332,14 +332,15 @@ exports.VERB_USAGE = {
|
|
|
332
332
|
],
|
|
333
333
|
},
|
|
334
334
|
"index-create": {
|
|
335
|
-
summary: "Create a DATABASE index (composite and non-unique included) by replaying the platform
|
|
335
|
+
summary: "Create a DATABASE index (composite and non-unique included) by replaying the platform Database Indexes dialog's processor calls, pinned to an update set, then read it back",
|
|
336
336
|
required: [
|
|
337
337
|
{ flag: "table", value: "<name>" },
|
|
338
338
|
{ flag: "columns", value: "<a[,b,...]>", note: "Comma-separated column elements, in index order." },
|
|
339
|
+
{ flag: "update-set", value: "<sys_id>", note: "Required on the live path; the index definition is captured into it. Must be in the table's application scope." },
|
|
339
340
|
],
|
|
340
341
|
optional: [
|
|
341
342
|
{ flag: "unique" },
|
|
342
|
-
{ flag: "access-method", value: "<m>", note: "Platform access method (default: the
|
|
343
|
+
{ flag: "access-method", value: "<m>", note: "Platform access method (default: btree, as the dialog does)." },
|
|
343
344
|
CONFIRM_FLAG,
|
|
344
345
|
FORCE_DRY_RUN_FLAG,
|
|
345
346
|
{ flag: "poll-attempts", value: "<n>", note: "Positive integer." },
|
|
@@ -349,11 +350,12 @@ exports.VERB_USAGE = {
|
|
|
349
350
|
],
|
|
350
351
|
gate: "confirm",
|
|
351
352
|
gateNote: "Nothing is sent OR read without --confirm.",
|
|
352
|
-
example: "dove-sn index-create --table x_cadso_journey_instance --columns state,created_on --confirm",
|
|
353
|
+
example: "dove-sn index-create --table x_cadso_journey_instance --columns state,created_on --update-set <sys_id> --confirm",
|
|
353
354
|
notes: [
|
|
354
|
-
"
|
|
355
|
-
"Idempotent: an index over exactly those columns returns already-exists with no write. --name is refused (the
|
|
356
|
-
"Needs a username+password identity that can form-log-in.",
|
|
355
|
+
"An index IS captured in an update set: the build job writes a sys_update_xml row (type=Indexes) into the user's CURRENT set, so --update-set is pinned first and the capture row is read back from it. The physical index is still built per instance; committing the set elsewhere rebuilds it.",
|
|
356
|
+
"Replays the dialog's own xmlhttp.do calls: IndexCreatorErrorChecker.canCreate (pre-flight) then ScheduleCreator.createSchedule. Idempotent: an index over exactly those columns returns already-exists with no write. --name is refused (the dialog has no name input; the real name is returned).",
|
|
357
|
+
"Needs a username+password identity that can form-log-in (xmlhttp.do ignores Basic auth / API keys).",
|
|
358
|
+
"Exit 2 when the index was scheduled but never read back from v_db_index, and when it was read back but its capture row was not found in the pinned set.",
|
|
357
359
|
],
|
|
358
360
|
},
|
|
359
361
|
"set-field": {
|
|
@@ -406,7 +408,7 @@ exports.VERB_USAGE = {
|
|
|
406
408
|
{
|
|
407
409
|
flag: "update-set",
|
|
408
410
|
value: "<sys_id>",
|
|
409
|
-
note: "
|
|
411
|
+
note: "Must exist and be in progress (checked on the dry-run too); pinned as current before the delete.",
|
|
410
412
|
},
|
|
411
413
|
],
|
|
412
414
|
optional: [
|
|
@@ -418,10 +420,36 @@ exports.VERB_USAGE = {
|
|
|
418
420
|
gateNote: "The dry-run prints the record snapshot that would be deleted.",
|
|
419
421
|
example: "dove-sn delete-record --table x_cadso_core_metric_point_type --sys-id <32-hex sys_id> --update-set <sys_id> --apply",
|
|
420
422
|
notes: [
|
|
421
|
-
"Reads the record BEFORE (a missing record is an error, never a no-op delete) and AFTER (exit 2 if it is still present).",
|
|
422
|
-
"Until TenonHQ/Dovetail#297 ships server-side the delete op IGNORES --update-set and captures into the session's current update set —
|
|
423
|
+
"Reads the record BEFORE (a missing record is an error, never a no-op delete) and AFTER (exit 2 if it is still present — including when the server refused the delete with an error).",
|
|
424
|
+
"Until TenonHQ/Dovetail#297 ships server-side the delete op IGNORES --update-set and captures into the session's current update set — so the verb pins --update-set as current first (refusing, nothing deleted, if the pin does not read back) and reads the DELETE row back from sys_update_xml: exit 2 when it landed in a different set or the read-back failed. A table that writes no update-set capture at all exits 0 with a note (captureState \"none\") — that delete will not travel.",
|
|
423
425
|
],
|
|
424
426
|
},
|
|
427
|
+
"sync-ux-events": {
|
|
428
|
+
summary: "Register a UI component's now-ui.json actions as sys_ux_event records on its macroponent",
|
|
429
|
+
required: [
|
|
430
|
+
{ flag: "file", value: "<now-ui.json>", note: "Path to the component's now-ui.json; every component with `actions` is synced." },
|
|
431
|
+
],
|
|
432
|
+
optional: [
|
|
433
|
+
{ flag: "component", value: "<tag>", note: "Limit to one component tag, e.g. cadso-journey-builder." },
|
|
434
|
+
{
|
|
435
|
+
flag: "update-set",
|
|
436
|
+
value: "<sys_id>",
|
|
437
|
+
note: "Required with --apply — the event creates and the macroponent edit are captured here.",
|
|
438
|
+
},
|
|
439
|
+
{ flag: "apply", note: "Write (without it the run is a dry-run)." },
|
|
440
|
+
{ flag: "dry-run", note: "Force a dry-run even with --apply." },
|
|
441
|
+
JSON_FLAG,
|
|
442
|
+
],
|
|
443
|
+
gate: "apply",
|
|
444
|
+
gateNote: "The dry-run prints each action as ok / create / link / drift / ambiguous, plus orphans.",
|
|
445
|
+
example: "dove-sn sync-ux-events --file path/to/now-ui.json --component cadso-journey-builder --update-set <sys_id> --apply",
|
|
446
|
+
notes: [
|
|
447
|
+
"Macroponent = sys_ux_macroponent whose root_component is the sys_ux_lib_component with that tag (category component). Events are created in the macroponent's scope.",
|
|
448
|
+
"Append-only: missing events are created and their sys_ids appended to dispatched_events; existing entries are never dropped or reordered. Label/description drift and orphaned (linked but undeclared) events are reported, never changed.",
|
|
449
|
+
"Exit codes: 0 in sync / dry-run / applied and verified, 1 bad args or unreadable file, 2 unresolved or ambiguous component/event, or a write that did not verify.",
|
|
450
|
+
],
|
|
451
|
+
stringFlags: ["file", "component", "update-set"],
|
|
452
|
+
},
|
|
425
453
|
"host-assets": {
|
|
426
454
|
summary: "Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)",
|
|
427
455
|
required: [
|
package/dist/createRecord.d.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* The INSERT counterpart to `set-field`.
|
|
10
10
|
*
|
|
11
11
|
* NOT for schema tables (sys_db_object / sys_dictionary) — that's
|
|
12
|
-
* create-table / add-column.
|
|
12
|
+
* create-table / add-column. NOT for sys_update_set — that's
|
|
13
|
+
* `dove createUpdateSet`. To UPDATE an existing record, use `set-field`.
|
|
13
14
|
*/
|
|
14
15
|
import type { ServiceNowClient } from "./client";
|
|
15
16
|
import type { RecordWriteResult } from "./setField";
|
package/dist/createRecord.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* The INSERT counterpart to `set-field`.
|
|
11
11
|
*
|
|
12
12
|
* NOT for schema tables (sys_db_object / sys_dictionary) — that's
|
|
13
|
-
* create-table / add-column.
|
|
13
|
+
* create-table / add-column. NOT for sys_update_set — that's
|
|
14
|
+
* `dove createUpdateSet`. To UPDATE an existing record, use `set-field`.
|
|
14
15
|
*/
|
|
15
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
17
|
exports.createRecord = createRecord;
|
|
@@ -19,6 +20,11 @@ const setField_1 = require("./setField");
|
|
|
19
20
|
// Same guard as set-field: schema tables are never written as data — routed to
|
|
20
21
|
// the dedicated schema verbs instead so we never orphan or corrupt metadata.
|
|
21
22
|
var REFUSED_TABLES = ["sys_db_object", "sys_dictionary"];
|
|
23
|
+
// The generic createRecord op never sets `application`, so an update set
|
|
24
|
+
// inserted through it lands in the session app while the read-back (name only)
|
|
25
|
+
// still reports success. Update sets go through the scope-correct
|
|
26
|
+
// createUpdateSet op instead.
|
|
27
|
+
var UPDATE_SET_TABLE = "sys_update_set";
|
|
22
28
|
async function createRecord(params) {
|
|
23
29
|
var client = params.client || (0, client_1.createClient)({});
|
|
24
30
|
var table = params.table;
|
|
@@ -29,6 +35,11 @@ async function createRecord(params) {
|
|
|
29
35
|
throw new Error("create-record: refusing to write " + table + " as data — it is a schema table. "
|
|
30
36
|
+ "Use add-column / create-table for schema changes.");
|
|
31
37
|
}
|
|
38
|
+
if (table === UPDATE_SET_TABLE) {
|
|
39
|
+
throw new Error("create-record: refusing to create a sys_update_set record — the generic createRecord op "
|
|
40
|
+
+ "does not set its application, so the set would land in the session app. "
|
|
41
|
+
+ "Use `dove createUpdateSet --name <name> --scope <scope>` instead.");
|
|
42
|
+
}
|
|
32
43
|
var fieldNames = params.fields ? Object.keys(params.fields) : [];
|
|
33
44
|
if (fieldNames.length === 0) {
|
|
34
45
|
throw new Error("create-record: at least one field (--fields key=value) is required.");
|