@tenonhq/dovetail-servicenow 0.0.45 → 0.0.47

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 CHANGED
@@ -652,6 +652,73 @@ Three consequences, each reported rather than hidden:
652
652
  single request goes out. Exit codes: `0` created / skipped / dry-run, `1` bad args, `2`
653
653
  failed (the lying-row case included).
654
654
 
655
+ ### List a table's indexes
656
+
657
+ ```bash
658
+ npx dove-sn index-list --table x_cadso_automate_message_batch_recipient --json
659
+ ```
660
+
661
+ Read-only: no form session, no writes. **`v_db_index` is the index read surface** - and
662
+ the only one. `sys_index` fails an API-LEVEL ACL (HTTP 403) for every identity including
663
+ admin, and `sys_index_column` does not exist at all (HTTP 400 `Invalid table`), so there
664
+ is no two-table index model to join and nothing to cross-check against.
665
+
666
+ Each row comes back as `{ name, columns, type, rawColumns }`. `columns` is the view's
667
+ bracketed `column_names` cell (`"[phone]"`, `"[a,b]"`) **parsed** into a list - never
668
+ substring-matched, because `"[owner_id]"` contains `"owner"`. `type` is `access_method`.
669
+
670
+ **Uniqueness is not readable.** `v_db_index` has no uniqueness field, so a unique index
671
+ and an ordinary one are indistinguishable in it: `unique` is left **absent** rather than
672
+ guessed, and `uniqueness-enforced` is reported in `unverified` on every result. Only a
673
+ duplicate-insert test proves enforcement. An empty result more likely means the table name
674
+ is wrong than that the table is unindexed - every physical table has a `PRIMARY`.
675
+
676
+ ### Create an index (composite and non-unique included)
677
+
678
+ > **A DATABASE INDEX IS A PHYSICAL, PER-INSTANCE CHANGE. IT IS NOT CAPTURED IN AN UPDATE
679
+ > SET AND DOES NOT TRAVEL WITH A PROMOTION.** Re-run `index-create` against every
680
+ > environment that needs the index (dev, test, uat, staging, prod). There is deliberately
681
+ > no `--update-set` - passing one is an error, not a silent no-op.
682
+
683
+ ```bash
684
+ # Dry-run (the DEFAULT) - sends nothing and reads nothing
685
+ npx dove-sn index-create --table x_cadso_journey_instance --columns state,created_on
686
+
687
+ # Send it
688
+ npx dove-sn index-create \
689
+ --table x_cadso_journey_instance --columns state,created_on --confirm --json
690
+ ```
691
+
692
+ This is what `add-index` cannot do. `sys_dictionary.unique` - the only record-shaped lever
693
+ - is **per-column and unique-only**, so composite and plain indexes have no record path at
694
+ all. `index-create` instead replays the platform's own index-creator form
695
+ (`sys_action=create_index`, `sysparm_index_table`, `sysparm_fields`,
696
+ `sysparm_unique_index_SKIP`) over a form-login session. That contract is lifted from the
697
+ instance's shipped `index_creator_information` UI macro, not from a guess, and the POST
698
+ target is taken from the rendered page's own `<form action>`.
699
+
700
+ - **Dry-run by default.** Without `--confirm` nothing is sent *and nothing is read*;
701
+ `--dry-run` forces a plan even with `--confirm`.
702
+ - **Idempotent.** On the live path `v_db_index` is read first, and an index over *exactly*
703
+ these columns short-circuits to `already-exists` with no form session and no write.
704
+ Column **order** is part of an index's identity - `[a,b]` is not `[b,a]`.
705
+ - **`--name` is refused.** The platform's form has no name input; ServiceNow names the
706
+ index itself. Reporting a name the instance does not carry would be a lie, so the
707
+ created index's *real* name is returned in `name` instead.
708
+ - **The read-back is the proof.** After the POST the index is polled for in `v_db_index`
709
+ (default 10 checks, 3 s apart - a build on a populated table is asynchronous). If it
710
+ never appears the status is `failed`: a form processor returning a page is not evidence
711
+ an ALTER ran, and a unique index cannot build over duplicate values (EMPTY counts).
712
+ - **Uniqueness is still never claimed.** `uniqueness-enforced` stays in `unverified` on
713
+ every status.
714
+
715
+ **Requires a username+password identity that can form-log-in.** An instance on
716
+ API-key-only auth, SSO or MFA rejects the form login however valid the API key is; the
717
+ verb fails at the session with that diagnosis rather than a mystery 302, and no `.do`
718
+ replay (including `create-table`'s) can work in that state.
719
+
720
+ Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed.
721
+
655
722
  ### Set a field on a record
656
723
 
657
724
  Set scalar field value(s) on an **existing** data record, capture the change into
@@ -954,7 +1021,12 @@ Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
954
1021
  `set_related_lists`, `add_choices_to_field`, the schema verbs `create_table` /
955
1022
  `add_column` / `add_index` (a single-column unique index via `sys_dictionary.unique`,
956
1023
  read back from the `v_db_index` view - uniqueness enforcement is always reported
957
- unverified), the record-write verbs `set_field` (update scalar fields on an
1024
+ unverified) / `index_list` (read-only: a table's database indexes from `v_db_index`,
1025
+ the only index read surface - `sys_index` is API-level-ACL 403 and `sys_index_column`
1026
+ does not exist) / `index_create` (create an index, composite and non-unique included, by
1027
+ replaying the platform index-creator form; dry-run by default, idempotent, read back from
1028
+ `v_db_index` - and **not** captured in an update set, because a database index is a
1029
+ physical per-instance change), the record-write verbs `set_field` (update scalar fields on an
958
1030
  existing record) and `create_record` (insert one record) — both update-set-captured
959
1031
  and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow Designer
960
1032
  tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
package/dist/cli.js CHANGED
@@ -1300,6 +1300,25 @@ function printHelp() {
1300
1300
  " is treated exactly like a proven collision. Success is\n" +
1301
1301
  " read back from v_db_index; that view has no uniqueness field, so\n" +
1302
1302
  " ENFORCEMENT is always reported unverified.\n" +
1303
+ " index-list List a table's DATABASE INDEXES from the v_db_index view (read-only)\n" +
1304
+ " (--table <name> [--json])\n" +
1305
+ " sys_index is API-level-ACL 403 and sys_index_column does not\n" +
1306
+ " exist, so v_db_index is the only index read surface. It has no\n" +
1307
+ " uniqueness field, so WHICH indexes are unique is always\n" +
1308
+ " reported unverified.\n" +
1309
+ " index-create Create a DATABASE INDEX (composite and non-unique included) by\n" +
1310
+ " replaying the platform index-creator form, then read it back\n" +
1311
+ " DRY-RUN BY DEFAULT — nothing is sent or read without --confirm\n" +
1312
+ " (--table <name> --columns <a[,b,...]> [--unique]\n" +
1313
+ " [--access-method <m>] [--confirm] [--dry-run]\n" +
1314
+ " [--poll-attempts <n>] [--poll-interval-ms <n>] [--debug] [--json])\n" +
1315
+ " A DATABASE INDEX IS PHYSICAL AND PER-INSTANCE: it is NOT captured\n" +
1316
+ " in an update set and does NOT travel with a promotion — re-run it\n" +
1317
+ " against every environment. There is no --update-set for that\n" +
1318
+ " reason. Idempotent: an index over exactly those columns already\n" +
1319
+ " present returns already-exists with no write. --name is REFUSED —\n" +
1320
+ " the platform's form has no name input; the real name is returned.\n" +
1321
+ " Needs a username+password identity that can form-log-in.\n" +
1303
1322
  " set-column Update an EXISTING column's SCHEMA (label/mandatory/default/read-only/max-length),\n" +
1304
1323
  " into an update set, then verify against the instance\n" +
1305
1324
  " (--table <t> --column <c> --update-set <sys_id>\n" +
@@ -1662,6 +1681,130 @@ async function runAddIndex(flags) {
1662
1681
  return 2;
1663
1682
  return 0;
1664
1683
  }
1684
+ /**
1685
+ * dove-sn index-list:
1686
+ * --table x_cadso_automate_message_batch_recipient [--json]
1687
+ *
1688
+ * Read-only. Lists the table's database indexes from the v_db_index view — the only
1689
+ * index read surface an instance exposes (sys_index is API-level-ACL 403,
1690
+ * sys_index_column does not exist).
1691
+ *
1692
+ * Exit codes: 0 read, 1 bad args.
1693
+ */
1694
+ async function runIndexList(flags) {
1695
+ if (!flags.table) {
1696
+ process.stderr.write("index-list: --table <name> is required\n");
1697
+ return 1;
1698
+ }
1699
+ var result = await (0, table_1.listIndexes)({
1700
+ client: (0, client_1.createClient)({}),
1701
+ table: flags.table,
1702
+ });
1703
+ if (flags.json === "true") {
1704
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1705
+ return 0;
1706
+ }
1707
+ process.stdout.write(result.table + " — " + result.indexes.length + " index(es)\n");
1708
+ for (var i = 0; i < result.indexes.length; i += 1) {
1709
+ var idx = result.indexes[i];
1710
+ process.stdout.write(" " +
1711
+ (idx.name || "(unnamed)") +
1712
+ " [" +
1713
+ idx.columns.join(", ") +
1714
+ "] " +
1715
+ (idx.type || "(no access_method)") +
1716
+ "\n");
1717
+ }
1718
+ process.stdout.write(result.note + "\nUNVERIFIED: " + result.unverified.join(", ") + "\n");
1719
+ return 0;
1720
+ }
1721
+ /**
1722
+ * dove-sn index-create:
1723
+ * --table x_cadso_core_u_smoke --columns a,b [--unique] [--access-method <m>]
1724
+ * [--confirm] [--dry-run] [--poll-attempts <n>] [--poll-interval-ms <n>]
1725
+ * [--debug] [--json]
1726
+ *
1727
+ * DRY-RUN BY DEFAULT — nothing is sent (and nothing is even READ) without --confirm;
1728
+ * --dry-run forces a plan even with it. There is deliberately NO --update-set: a
1729
+ * database index is physical and is not captured in one.
1730
+ *
1731
+ * Exit codes: 0 created / already-exists / dry-run, 1 bad args, 2 failed (which
1732
+ * includes "the form was posted but no index was read back").
1733
+ */
1734
+ async function runIndexCreate(flags) {
1735
+ var columns = splitList(flags.columns || "");
1736
+ if (!flags.table || columns.length === 0) {
1737
+ process.stderr.write("index-create: --table <name> and --columns <a[,b,...]> are required\n");
1738
+ return 1;
1739
+ }
1740
+ if (flags["update-set"]) {
1741
+ process.stderr.write("index-create: --update-set is not accepted. A database index is a PHYSICAL, " +
1742
+ "PER-INSTANCE change — it is NOT captured in an update set and does not " +
1743
+ "travel with a promotion. Run index-create against each environment.\n");
1744
+ return 1;
1745
+ }
1746
+ var params = {
1747
+ client: (0, client_1.createClient)({}),
1748
+ table: flags.table,
1749
+ columns: columns,
1750
+ unique: flags.unique === "true",
1751
+ confirm: flags.confirm === "true",
1752
+ dryRun: flags["dry-run"] === "true",
1753
+ };
1754
+ if (flags.name !== undefined)
1755
+ params.name = flags.name;
1756
+ if (flags["access-method"])
1757
+ params.accessMethod = flags["access-method"];
1758
+ if (flags["form-path"])
1759
+ params.formPath = flags["form-path"];
1760
+ if (flags.debug === "true")
1761
+ params.debug = true;
1762
+ // A non-integer poll setting would make the bounded wait unbounded (or zero).
1763
+ // Reject it here with a named message instead of letting NaN reach the loop.
1764
+ var numeric = [
1765
+ ["poll-attempts", "pollAttempts"],
1766
+ ["poll-interval-ms", "pollIntervalMs"],
1767
+ ];
1768
+ for (var n = 0; n < numeric.length; n += 1) {
1769
+ var raw = flags[numeric[n][0]];
1770
+ if (raw === undefined)
1771
+ continue;
1772
+ var value = Number(raw);
1773
+ if (!Number.isInteger(value) || value <= 0) {
1774
+ process.stderr.write("index-create: --" +
1775
+ numeric[n][0] +
1776
+ " must be a positive integer (got '" +
1777
+ raw +
1778
+ "')\n");
1779
+ return 1;
1780
+ }
1781
+ params[numeric[n][1]] = value;
1782
+ }
1783
+ var result = await (0, table_1.createIndex)(params);
1784
+ if (flags.json === "true") {
1785
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1786
+ }
1787
+ else {
1788
+ process.stdout.write("[" +
1789
+ result.status +
1790
+ "] " +
1791
+ result.table +
1792
+ " [" +
1793
+ result.columns.join(", ") +
1794
+ "]" +
1795
+ (result.name ? " -> " + result.name : "") +
1796
+ (result.instance ? " on " + result.instance : "") +
1797
+ (result.verified ? " — verified" : "") +
1798
+ "\n" +
1799
+ result.note +
1800
+ "\nUNVERIFIED: " +
1801
+ result.unverified.join(", ") +
1802
+ "\n");
1803
+ }
1804
+ if (result.status === "failed")
1805
+ return 2;
1806
+ return 0;
1807
+ }
1665
1808
  /** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
1666
1809
  * false. Anything else is rejected rather than quietly coerced to `true`. */
1667
1810
  function parseBoolFlag(name, raw, verb = "set-column") {
@@ -2544,6 +2687,12 @@ async function main() {
2544
2687
  if (parsed.command === "add-index") {
2545
2688
  return await runAddIndex(parsed.flags);
2546
2689
  }
2690
+ if (parsed.command === "index-list") {
2691
+ return await runIndexList(parsed.flags);
2692
+ }
2693
+ if (parsed.command === "index-create") {
2694
+ return await runIndexCreate(parsed.flags);
2695
+ }
2547
2696
  if (parsed.command === "set-column") {
2548
2697
  return await runSetColumn(parsed.flags, parsed.bare);
2549
2698
  }
package/dist/index.d.ts CHANGED
@@ -21,8 +21,8 @@ export { sincPlugin } from "./plugin";
21
21
  export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, resolveScope, slugInternalName, fetchActionSteps, findStepInput, triggerPublication, publishActionType, editActionType, defineActionType, planActionDefinition, validateDefineSpec, applyStepOps, verifySteps, summarizeSteps, formatStepPill, readFlow, readActionType, publishFlow, copyFlow, createFlow, buildPublishModel, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError, } from "./flowDesigner";
22
22
  export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, CloneActionTypePlan, CloneActionTypeStepReport, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, EditActionTypeParams, EditActionTypeResult, EditActionTypeOps, DefineActionSpec, DefineActionInputSpec, DefineActionOutputSpec, DefineStepSpec, DefineStepVarSpec, DefineStepOutputSpec, DefineStepValue, DefineActionTypeParams, DefineActionTypeResult, DefineActionDiff, DefineActionPlan, PlanActionDefinitionParams, ActionView, StepView, StepOps, StepRecord, StepSummary, StepIoSummary, PatchStepScriptOp, SetStepInputOp, AddStepOutputOp, AddStepInputOp, ApplyStepOpsResult, VerifyStepsResult, ReadFlowParams, ReadFlowResult, FlowStep, FlowVariable, ReadActionTypeParams, ReadActionTypeResult, ActionIo, PublishFlowParams, PublishFlowResult, CopyFlowParams, CopyFlowResult, CreateFlowParams, CreateFlowResult, EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch, TestFlowParams, TestFlowResult, WriteOp, WriteOpResult, } from "./flowDesigner";
23
23
  export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, RemoveChoicesParams, RemoveChoicesResult, ChoiceRemovalResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult, } from "./types";
24
- export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, addIndex, parseIndexColumns, indexMatchesColumns, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
25
- export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, AddIndexParams, AddIndexResult, AddIndexVerification, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
24
+ export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, addIndex, parseIndexColumns, indexMatchesColumns, listIndexes, createIndex, validateCreateIndex, DEFAULT_INDEX_FORM_PATH, NOT_IN_UPDATE_SET, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
25
+ export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, AddIndexParams, AddIndexResult, AddIndexVerification, ListIndexesParams, ListIndexesResult, TableIndex, CreateIndexParams, CreateIndexResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
26
26
  export { setField } from "./setField";
27
27
  export { createRecord } from "./createRecord";
28
28
  export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setField";
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.formatStepPill = exports.summarizeSteps = exports.verifySteps = exports.applyStepOps = exports.validateDefineSpec = exports.planActionDefinition = exports.defineActionType = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.findStepInput = exports.fetchActionSteps = exports.slugInternalName = exports.resolveScope = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatRemoveChoicesResult = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.ChoiceWriteError = exports.removeChoicesFromField = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.assertWriteAllowed = exports.evaluateWriteGate = exports.isCiEnvironment = exports.resolveExecutionContext = exports.PROCESSFLOW_PATH_PREFIX = exports.isProcessflowPath = exports.resolveFlowAuth = exports.createClient = void 0;
10
- exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.PUBLISH_TARGETS = exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = void 0;
11
- exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = void 0;
10
+ exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.PUBLISH_TARGETS = exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.NOT_IN_UPDATE_SET = exports.DEFAULT_INDEX_FORM_PATH = exports.validateCreateIndex = exports.createIndex = exports.listIndexes = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = void 0;
11
+ exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = void 0;
12
12
  var client_1 = require("./client");
13
13
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
14
14
  Object.defineProperty(exports, "resolveFlowAuth", { enumerable: true, get: function () { return client_1.resolveFlowAuth; } });
@@ -94,6 +94,11 @@ Object.defineProperty(exports, "deriveElement", { enumerable: true, get: functio
94
94
  Object.defineProperty(exports, "addIndex", { enumerable: true, get: function () { return table_1.addIndex; } });
95
95
  Object.defineProperty(exports, "parseIndexColumns", { enumerable: true, get: function () { return table_1.parseIndexColumns; } });
96
96
  Object.defineProperty(exports, "indexMatchesColumns", { enumerable: true, get: function () { return table_1.indexMatchesColumns; } });
97
+ Object.defineProperty(exports, "listIndexes", { enumerable: true, get: function () { return table_1.listIndexes; } });
98
+ Object.defineProperty(exports, "createIndex", { enumerable: true, get: function () { return table_1.createIndex; } });
99
+ Object.defineProperty(exports, "validateCreateIndex", { enumerable: true, get: function () { return table_1.validateCreateIndex; } });
100
+ Object.defineProperty(exports, "DEFAULT_INDEX_FORM_PATH", { enumerable: true, get: function () { return table_1.DEFAULT_INDEX_FORM_PATH; } });
101
+ Object.defineProperty(exports, "NOT_IN_UPDATE_SET", { enumerable: true, get: function () { return table_1.NOT_IN_UPDATE_SET; } });
97
102
  Object.defineProperty(exports, "setColumn", { enumerable: true, get: function () { return table_1.setColumn; } });
98
103
  Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return table_1.resolveAttributes; } });
99
104
  Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return table_1.toStoredValue; } });
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
12
12
  import type { ServiceNowClient } from "../client";
13
- export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "remove_choices_from_field", "flow_view", "action_view", "action_edit", "action_clone", "action_define", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "add_index", "set_column", "set_table", "set_field", "create_record", "host_assets", "invoke_rest", "app_publish", "update_set_export", "app_export"];
13
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "remove_choices_from_field", "flow_view", "action_view", "action_edit", "action_clone", "action_define", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "add_index", "index_list", "index_create", "set_column", "set_table", "set_field", "create_record", "host_assets", "invoke_rest", "app_publish", "update_set_export", "app_export"];
14
14
  export type ToolName = (typeof TOOL_NAMES)[number];
15
15
  export interface RegistryDeps {
16
16
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -57,6 +57,8 @@ exports.TOOL_NAMES = [
57
57
  "create_table",
58
58
  "add_column",
59
59
  "add_index",
60
+ "index_list",
61
+ "index_create",
60
62
  "set_column",
61
63
  "set_table",
62
64
  "set_field",
@@ -509,6 +511,72 @@ function buildDescriptors(deps = {}) {
509
511
  });
510
512
  },
511
513
  },
514
+ {
515
+ name: "index_list",
516
+ annotations: dovetail_mcp_kit_1.READ_ONLY,
517
+ description: "List the DATABASE INDEXES on a ServiceNow table. Read-only — no form session, no " +
518
+ "writes. Reads the v_db_index VIEW, which is the only index read surface an instance " +
519
+ "exposes: sys_index fails an API-LEVEL ACL (HTTP 403) for every identity including " +
520
+ "admin (an ACL that refuses GET refuses POST), and sys_index_column does not exist at " +
521
+ "all (HTTP 400 'Invalid table') — there is no two-table index model to join. Each row " +
522
+ "returns { name, columns, type, rawColumns }: `columns` is v_db_index's bracketed " +
523
+ "`column_names` cell ('[phone]', '[a,b]') PARSED into a list, never substring-matched, " +
524
+ "and `type` is access_method (btree for essentially everything). UNIQUENESS IS NOT " +
525
+ "READABLE: v_db_index carries no uniqueness field, so a unique index and an ordinary " +
526
+ "one are indistinguishable in it — `unique` is therefore left ABSENT rather than " +
527
+ "guessed, and 'uniqueness-enforced' is always reported in unverified. Only a " +
528
+ "duplicate-insert test proves enforcement. An empty result more likely means the table " +
529
+ "name is wrong than that the table is unindexed (every physical table has a PRIMARY).",
530
+ shape: schemas_1.listIndexesSchema.shape,
531
+ handler: async function (args) {
532
+ var p = schemas_1.listIndexesSchema.parse(args);
533
+ return (0, table_1.listIndexes)({ client: client(), table: p.table });
534
+ },
535
+ },
536
+ {
537
+ name: "index_create",
538
+ annotations: dovetail_mcp_kit_1.WRITE_ADDITIVE_IDEMPOTENT,
539
+ description: "Create a DATABASE INDEX on an EXISTING ServiceNow table — including the COMPOSITE and " +
540
+ "NON-UNIQUE indexes add_index cannot build. A DATABASE INDEX IS A PHYSICAL, PER-INSTANCE " +
541
+ "CHANGE: IT IS NOT CAPTURED IN AN UPDATE SET AND DOES NOT TRAVEL WITH A PROMOTION, so it " +
542
+ "must be re-run against every environment that needs it — which is why this tool takes " +
543
+ "no updateSetSysId. There is no record path to an index (sys_index is API-level-ACL 403, " +
544
+ "sys_index_column does not exist, and sys_dictionary.unique — the add_index lever — is " +
545
+ "per-column and unique-only), so this replays the platform's own index-creator form " +
546
+ "(sys_action=create_index) over a form-login session. DRY-RUN BY DEFAULT: without " +
547
+ "confirm:true NOTHING is sent and nothing is read; dryRun:true forces a dry-run even " +
548
+ "with confirm. IDEMPOTENT: on the live path v_db_index is read first, and an index over " +
549
+ "exactly these columns returns 'already-exists' with no write. `columns` is ORDERED — " +
550
+ "order is part of an index's identity and is preserved verbatim. `name` is REFUSED: the " +
551
+ "platform's form has no name input (ServiceNow names the index itself), so accepting one " +
552
+ "would mean reporting a name the instance does not carry — the created index's real name " +
553
+ "comes back in `name`. After the POST the index is polled for in v_db_index (default 10 " +
554
+ "checks, 3s apart, since a build on a populated table is asynchronous); if it never " +
555
+ "appears the result is 'failed', because a form processor returning a page is not " +
556
+ "evidence an ALTER ran — and a unique index cannot build over duplicate values, EMPTY " +
557
+ "included. verified:true means a matching row was READ BACK. 'uniqueness-enforced' is " +
558
+ "ALWAYS in unverified: v_db_index has no uniqueness field, so enforcement is provable " +
559
+ "only by a duplicate-insert test. Requires a username+password identity that can form-" +
560
+ "log-in; an instance on API-key-only auth or SSO/MFA will fail at the session with a " +
561
+ "diagnosis, because no .do replay can work there.",
562
+ shape: schemas_1.createIndexSchema.shape,
563
+ handler: async function (args) {
564
+ var p = schemas_1.createIndexSchema.parse(args);
565
+ return (0, table_1.createIndex)({
566
+ client: client(),
567
+ table: p.table,
568
+ columns: p.columns,
569
+ unique: p.unique,
570
+ name: p.name,
571
+ accessMethod: p.accessMethod,
572
+ confirm: p.confirm,
573
+ dryRun: p.dryRun,
574
+ pollAttempts: p.pollAttempts,
575
+ pollIntervalMs: p.pollIntervalMs,
576
+ debug: p.debug,
577
+ });
578
+ },
579
+ },
512
580
  {
513
581
  name: "set_column",
514
582
  annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
@@ -1930,6 +1930,47 @@ export declare var addIndexSchema: z.ZodObject<{
1930
1930
  scope?: string | undefined;
1931
1931
  updateSetSysId?: string | undefined;
1932
1932
  }>;
1933
+ export declare var listIndexesSchema: z.ZodObject<{
1934
+ table: z.ZodString;
1935
+ }, "strip", z.ZodTypeAny, {
1936
+ table: string;
1937
+ }, {
1938
+ table: string;
1939
+ }>;
1940
+ export declare var createIndexSchema: z.ZodObject<{
1941
+ table: z.ZodString;
1942
+ columns: z.ZodArray<z.ZodString, "many">;
1943
+ unique: z.ZodOptional<z.ZodBoolean>;
1944
+ name: z.ZodOptional<z.ZodString>;
1945
+ accessMethod: z.ZodOptional<z.ZodString>;
1946
+ confirm: z.ZodOptional<z.ZodBoolean>;
1947
+ dryRun: z.ZodOptional<z.ZodBoolean>;
1948
+ pollAttempts: z.ZodOptional<z.ZodNumber>;
1949
+ pollIntervalMs: z.ZodOptional<z.ZodNumber>;
1950
+ debug: z.ZodOptional<z.ZodBoolean>;
1951
+ }, "strip", z.ZodTypeAny, {
1952
+ columns: string[];
1953
+ table: string;
1954
+ name?: string | undefined;
1955
+ confirm?: boolean | undefined;
1956
+ dryRun?: boolean | undefined;
1957
+ debug?: boolean | undefined;
1958
+ unique?: boolean | undefined;
1959
+ accessMethod?: string | undefined;
1960
+ pollAttempts?: number | undefined;
1961
+ pollIntervalMs?: number | undefined;
1962
+ }, {
1963
+ columns: string[];
1964
+ table: string;
1965
+ name?: string | undefined;
1966
+ confirm?: boolean | undefined;
1967
+ dryRun?: boolean | undefined;
1968
+ debug?: boolean | undefined;
1969
+ unique?: boolean | undefined;
1970
+ accessMethod?: string | undefined;
1971
+ pollAttempts?: number | undefined;
1972
+ pollIntervalMs?: number | undefined;
1973
+ }>;
1933
1974
  export declare var columnAttributesSchema: z.ZodObject<{
1934
1975
  label: z.ZodOptional<z.ZodString>;
1935
1976
  mandatory: z.ZodOptional<z.ZodBoolean>;
@@ -4,7 +4,7 @@
4
4
  * own file so registry.ts stays focused on wiring.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.invokeRestSchema = exports.publishAppSchema = exports.exportAppSchema = exports.exportUpdateSetSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setTableSchema = exports.tableAttributesSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addIndexSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.defineActionSchema = exports.defineActionSpecSchema = exports.cloneActionSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.removeChoicesFromFieldSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
7
+ exports.invokeRestSchema = exports.publishAppSchema = exports.exportAppSchema = exports.exportUpdateSetSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setTableSchema = exports.tableAttributesSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.createIndexSchema = exports.listIndexesSchema = exports.addIndexSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.defineActionSchema = exports.defineActionSpecSchema = exports.cloneActionSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.removeChoicesFromFieldSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
8
8
  const zod_1 = require("zod");
9
9
  exports.createViewSchema = zod_1.z.object({
10
10
  name: zod_1.z.string().min(1),
@@ -378,6 +378,31 @@ exports.addIndexSchema = zod_1.z.object({
378
378
  dryRun: zod_1.z.boolean().optional(),
379
379
  debug: zod_1.z.boolean().optional(),
380
380
  });
381
+ // index_list is read-only: a table name and nothing else. `v_db_index` keys on the table
382
+ // NAME, not a sys_id, so no sys_id form is offered — accepting one would mean resolving it
383
+ // and then listing a table the caller never named.
384
+ exports.listIndexesSchema = zod_1.z.object({
385
+ table: zod_1.z.string().min(1),
386
+ });
387
+ // index_create takes an ordered column LIST — order is part of an index's identity, so it
388
+ // is preserved verbatim and never sorted. `name` is accepted ONLY so that asking for one
389
+ // earns the explanation that the platform's index-creator form has no name input (the
390
+ // same contract set-column uses for internalType/element); createIndex refuses it.
391
+ // confirm is the write gate: without confirm:true the tool is a pure dry-run that sends
392
+ // nothing and reads nothing. There is deliberately NO updateSetSysId — a database index is
393
+ // physical and is not captured in an update set.
394
+ exports.createIndexSchema = zod_1.z.object({
395
+ table: zod_1.z.string().min(1),
396
+ columns: zod_1.z.array(zod_1.z.string().min(1)).min(1),
397
+ unique: zod_1.z.boolean().optional(),
398
+ name: zod_1.z.string().optional(),
399
+ accessMethod: zod_1.z.string().min(1).optional(),
400
+ confirm: zod_1.z.boolean().optional(),
401
+ dryRun: zod_1.z.boolean().optional(),
402
+ pollAttempts: zod_1.z.number().int().positive().optional(),
403
+ pollIntervalMs: zod_1.z.number().int().positive().optional(),
404
+ debug: zod_1.z.boolean().optional(),
405
+ });
381
406
  // set-column takes a CLOSED attribute set, not an open field map: an unbounded write to
382
407
  // sys_dictionary lets a caller silently corrupt the schema. internalType and element are
383
408
  // listed but are NOT settable — ServiceNow honours neither on an existing column, and
@@ -0,0 +1,118 @@
1
+ /**
2
+ * createIndex — create a database index on an EXISTING ServiceNow table, including
3
+ * the COMPOSITE and NON-UNIQUE indexes `add-index` cannot build, by replaying the
4
+ * platform's own index-creator form.
5
+ *
6
+ * ──────────────────────────────────────────────────────────────────────────────
7
+ * A DATABASE INDEX IS A PHYSICAL, PER-INSTANCE CHANGE. IT IS **NOT** CAPTURED IN AN
8
+ * UPDATE SET, AND IT DOES **NOT** TRAVEL WITH A PROMOTION. Every environment that
9
+ * needs the index must have this run against it. That is why this verb takes no
10
+ * `updateSetSysId` — there is nothing for one to capture.
11
+ * ──────────────────────────────────────────────────────────────────────────────
12
+ *
13
+ * WHY A FORM REPLAY. There is no record path to an index:
14
+ * - `sys_index` is API-level-ACL 403 for every identity (an ACL that refuses GET
15
+ * refuses POST), and `sys_index_column` does not exist (HTTP 400 "Invalid
16
+ * table"). So the "insert two records" design is not merely blocked, it is
17
+ * not a real model of the platform.
18
+ * - `sys_dictionary.unique` is the one record-shaped lever, and it is PER-COLUMN
19
+ * and UNIQUE-ONLY. That lever is already shipped as `add-index`; this verb
20
+ * exists for everything that lever cannot express.
21
+ *
22
+ * THE FORM CONTRACT IS THE INSTANCE'S OWN. It was not guessed and it was not read
23
+ * off a stale HAR: it is lifted from the shipped `index_creator_information`
24
+ * sys_ui_macro and the `v_index_creator` "New" UI action, both read live from a
25
+ * Tenon instance 2026-09-30. The macro's own inputs are:
26
+ *
27
+ * sys_action = "create_index" (hidden)
28
+ * sysparm_index_table = <table>
29
+ * sysparm_fields = <comma-separated column list>
30
+ * sysparm_unique_index_SKIP = "on" (the Unique Index checkbox)
31
+ * sysparm_access_method = <method> (only when
32
+ * glide.ui.index_method.enabled is true)
33
+ *
34
+ * and the dialog is the `index_creator_dialog` UI page. The POST target is taken
35
+ * from that page's own `<form action>` when it renders one, falling back to the
36
+ * page itself — harvest-then-use, the same discipline createTable applies to the
37
+ * list-edit key rather than hard-coding a guess.
38
+ *
39
+ * THERE IS NO INDEX-NAME INPUT. The form has none; ServiceNow names the index
40
+ * itself (in practice after the leading column). A `name` is therefore REFUSED
41
+ * rather than silently dropped — a caller who asked for `idx_phone_created` and got
42
+ * `phone` would have no way to know, and the whole point of this module is that the
43
+ * instance is read back rather than believed.
44
+ *
45
+ * THE WRITE IS NEVER THE PROOF. After the POST the index is looked for in
46
+ * `v_db_index` on a bounded poll (index builds are asynchronous on a populated
47
+ * table). If it never appears, this FAILS loudly — a 200 from a form processor is
48
+ * not evidence an ALTER ran. `verified:true` means a matching row was READ BACK.
49
+ *
50
+ * WHAT IS STILL NOT PROVABLE: that a unique index actually REJECTS duplicates.
51
+ * `v_db_index` has no uniqueness field, so "uniqueness-enforced" is reported in
52
+ * `unverified` on every status, success included.
53
+ *
54
+ * ES6 only, no optional chaining, no `any`.
55
+ */
56
+ import type { ServiceNowClient } from "../client";
57
+ /** The UI page that renders the index creator. Overridable per instance. */
58
+ export declare var DEFAULT_INDEX_FORM_PATH: string;
59
+ /** The standing caveat every caller must carry forward. */
60
+ export declare var NOT_IN_UPDATE_SET: string;
61
+ export interface CreateIndexParams {
62
+ /** REST client — used for the idempotency read and the read-back poll. */
63
+ client: ServiceNowClient;
64
+ /** The table to index, by NAME. */
65
+ table: string;
66
+ /** Ordered column list. Order matters to an index; it is preserved verbatim. */
67
+ columns: Array<string>;
68
+ /**
69
+ * REFUSED. The platform's index-creator form has no name input — ServiceNow names
70
+ * the index itself. Declared only so a caller can express it and be told why,
71
+ * rather than have it silently ignored.
72
+ */
73
+ name?: string;
74
+ /** Tick the form's "Unique Index" checkbox. Default false (a plain index). */
75
+ unique?: boolean;
76
+ /** `sysparm_access_method`, when the instance exposes the selector. */
77
+ accessMethod?: string;
78
+ /** REQUIRED to write. Without it this is a dry-run, whatever else is passed. */
79
+ confirm?: boolean;
80
+ /** Force a dry-run even with confirm:true. */
81
+ dryRun?: boolean;
82
+ /** Override the index-creator page path (default DEFAULT_INDEX_FORM_PATH). */
83
+ formPath?: string;
84
+ /** Read-back poll shape. Defaults: 10 attempts, 3000 ms apart. */
85
+ pollAttempts?: number;
86
+ pollIntervalMs?: number;
87
+ /** Instance/creds for the form session (default: env, same precedence as the client). */
88
+ instance?: string;
89
+ user?: string;
90
+ password?: string;
91
+ /** Add diagnostic detail to the note. */
92
+ debug?: boolean;
93
+ }
94
+ export interface CreateIndexResult {
95
+ status: "created" | "already-exists" | "dry-run" | "failed";
96
+ /** True ONLY on a fresh, read-back-verified create. */
97
+ created: boolean;
98
+ table: string;
99
+ /** The index name as `v_db_index` reports it — never a name we asked for. */
100
+ name: string;
101
+ /** The requested column list, normalized. */
102
+ columns: Array<string>;
103
+ /** True ONLY when a matching `v_db_index` row was READ BACK. */
104
+ verified: boolean;
105
+ /** The host the write was aimed at — an index is per-instance, so this matters. */
106
+ instance: string;
107
+ /** Always includes "uniqueness-enforced". Never empty. */
108
+ unverified: Array<string>;
109
+ /** HTTP status of the form POST; 0 when none was sent. */
110
+ httpStatus: number;
111
+ note: string;
112
+ }
113
+ /** Validate every input BEFORE any network call. Returns the normalized columns. */
114
+ export declare function validateCreateIndex(params: CreateIndexParams): {
115
+ table: string;
116
+ columns: Array<string>;
117
+ };
118
+ export declare function createIndex(params: CreateIndexParams): Promise<CreateIndexResult>;