@tenonhq/dovetail-servicenow 0.0.33 → 0.0.34
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 +10 -0
- package/dist/mcp/registry.js +8 -1
- package/dist/table/index.d.ts +2 -0
- package/dist/table/index.js +13 -1
- package/dist/table/overrideColumn.d.ts +155 -0
- package/dist/table/overrideColumn.js +388 -0
- package/dist/table/setColumn.d.ts +27 -1
- package/dist/table/setColumn.js +267 -22
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -893,6 +893,10 @@ function printHelp() {
|
|
|
893
893
|
" A max-length SHRINK is REFUSED while rows hold longer values —\n" +
|
|
894
894
|
" ServiceNow silently ignores such a shrink (200 OK, no change).\n" +
|
|
895
895
|
" Shorten or clear those values first, then re-run.\n" +
|
|
896
|
+
" An INHERITED column (one defined on a parent table) is narrowed for\n" +
|
|
897
|
+
" YOUR table alone, via sys_dictionary_override / sys_documentation —\n" +
|
|
898
|
+
" the parent and its other children are untouched. max-length is the\n" +
|
|
899
|
+
" exception: it is the parent's physical column and is refused.\n" +
|
|
896
900
|
" --element / --internal-type are REFUSED with an explanation:\n" +
|
|
897
901
|
" ServiceNow silently ignores both on an existing column.\n" +
|
|
898
902
|
" set-table Update an EXISTING TABLE's own dictionary row (the collection row),\n" +
|
|
@@ -1214,6 +1218,12 @@ async function runSetColumn(flags, bare) {
|
|
|
1214
1218
|
result.table +
|
|
1215
1219
|
"." +
|
|
1216
1220
|
result.column +
|
|
1221
|
+
// Say when the change went to an override rather than the column's own row. The
|
|
1222
|
+
// caller asked for a table + column; without this they have no reason to expect
|
|
1223
|
+
// the write landed on a different record type entirely.
|
|
1224
|
+
(result.via === "override"
|
|
1225
|
+
? " — override (inherited from " + result.definedOn + ")"
|
|
1226
|
+
: "") +
|
|
1217
1227
|
(result.verified && result.status === "applied" ? " — verified" : "") +
|
|
1218
1228
|
(result.status === "applied" && !result.capturedInUpdateSet
|
|
1219
1229
|
? " — NOT CAPTURED"
|
package/dist/mcp/registry.js
CHANGED
|
@@ -370,7 +370,14 @@ function buildDescriptors(deps = {}) {
|
|
|
370
370
|
"refuses such a shrink (200 OK, column unchanged, data preserved), so the tool names the " +
|
|
371
371
|
"blocking rows instead of issuing a write that would be quietly ignored. Clear those values " +
|
|
372
372
|
"first, then re-run — there is no override, because forcing it would either do nothing or " +
|
|
373
|
-
"destroy data.
|
|
373
|
+
"destroy data. INHERITED COLUMNS ARE SUPPORTED: on an extended table the column is defined on " +
|
|
374
|
+
"an ancestor, and set_column narrows it for THAT TABLE ALONE via sys_dictionary_override " +
|
|
375
|
+
"(mandatory/default/readOnly) or sys_documentation (label) — the ancestor and every sibling " +
|
|
376
|
+
"table are left untouched, and the result reports via:'override' with definedOn set. Do NOT " +
|
|
377
|
+
"call set_column against the parent table to change a child's column: that changes it for " +
|
|
378
|
+
"EVERY descendant. maxLength is the sole exception — it is the ancestor's physical column, has " +
|
|
379
|
+
"no per-child override, and is refused with an explanation. dryRun:true diffs against the " +
|
|
380
|
+
"instance, flags the blocking rows, and writes nothing.",
|
|
374
381
|
shape: schemas_1.setColumnSchema.shape,
|
|
375
382
|
handler: async function (args) {
|
|
376
383
|
var p = schemas_1.setColumnSchema.parse(args);
|
package/dist/table/index.d.ts
CHANGED
|
@@ -15,5 +15,7 @@ export { addColumn, deriveElement } from "./addColumn";
|
|
|
15
15
|
export type { AddColumnParams, AddColumnResult } from "./addColumn";
|
|
16
16
|
export { setColumn, resolveAttributes, toStoredValue, findTruncationRisk, } from "./setColumn";
|
|
17
17
|
export type { SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, TruncationRisk, } from "./setColumn";
|
|
18
|
+
export { OVERRIDABLE, LABEL_LANGUAGE, explainMaxLengthNotOverridable, resolveTableScope, findOverrideRow, findLabelRow, effectiveValue, diffInherited, applyInheritedWrites, overrideUpdateName, labelUpdateName, } from "./overrideColumn";
|
|
19
|
+
export type { OverridableAttribute, InheritedChange, InheritedWriteParams, InheritedWriteResult, } from "./overrideColumn";
|
|
18
20
|
export { setTable, resolveTableAttributes } from "./setTable";
|
|
19
21
|
export type { SetTableParams, SetTableResult, TableAttributes, } from "./setTable";
|
package/dist/table/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* live-validation caveat.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.resolveTableAttributes = exports.setTable = exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
|
|
8
|
+
exports.resolveTableAttributes = exports.setTable = exports.labelUpdateName = exports.overrideUpdateName = exports.applyInheritedWrites = exports.diffInherited = exports.effectiveValue = exports.findLabelRow = exports.findOverrideRow = exports.resolveTableScope = exports.explainMaxLengthNotOverridable = exports.LABEL_LANGUAGE = exports.OVERRIDABLE = exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
|
|
9
9
|
var createTable_1 = require("./createTable");
|
|
10
10
|
Object.defineProperty(exports, "createTable", { enumerable: true, get: function () { return createTable_1.createTable; } });
|
|
11
11
|
Object.defineProperty(exports, "projectTableGraph", { enumerable: true, get: function () { return createTable_1.projectTableGraph; } });
|
|
@@ -42,6 +42,18 @@ Object.defineProperty(exports, "setColumn", { enumerable: true, get: function ()
|
|
|
42
42
|
Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return setColumn_1.resolveAttributes; } });
|
|
43
43
|
Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return setColumn_1.toStoredValue; } });
|
|
44
44
|
Object.defineProperty(exports, "findTruncationRisk", { enumerable: true, get: function () { return setColumn_1.findTruncationRisk; } });
|
|
45
|
+
var overrideColumn_1 = require("./overrideColumn");
|
|
46
|
+
Object.defineProperty(exports, "OVERRIDABLE", { enumerable: true, get: function () { return overrideColumn_1.OVERRIDABLE; } });
|
|
47
|
+
Object.defineProperty(exports, "LABEL_LANGUAGE", { enumerable: true, get: function () { return overrideColumn_1.LABEL_LANGUAGE; } });
|
|
48
|
+
Object.defineProperty(exports, "explainMaxLengthNotOverridable", { enumerable: true, get: function () { return overrideColumn_1.explainMaxLengthNotOverridable; } });
|
|
49
|
+
Object.defineProperty(exports, "resolveTableScope", { enumerable: true, get: function () { return overrideColumn_1.resolveTableScope; } });
|
|
50
|
+
Object.defineProperty(exports, "findOverrideRow", { enumerable: true, get: function () { return overrideColumn_1.findOverrideRow; } });
|
|
51
|
+
Object.defineProperty(exports, "findLabelRow", { enumerable: true, get: function () { return overrideColumn_1.findLabelRow; } });
|
|
52
|
+
Object.defineProperty(exports, "effectiveValue", { enumerable: true, get: function () { return overrideColumn_1.effectiveValue; } });
|
|
53
|
+
Object.defineProperty(exports, "diffInherited", { enumerable: true, get: function () { return overrideColumn_1.diffInherited; } });
|
|
54
|
+
Object.defineProperty(exports, "applyInheritedWrites", { enumerable: true, get: function () { return overrideColumn_1.applyInheritedWrites; } });
|
|
55
|
+
Object.defineProperty(exports, "overrideUpdateName", { enumerable: true, get: function () { return overrideColumn_1.overrideUpdateName; } });
|
|
56
|
+
Object.defineProperty(exports, "labelUpdateName", { enumerable: true, get: function () { return overrideColumn_1.labelUpdateName; } });
|
|
45
57
|
var setTable_1 = require("./setTable");
|
|
46
58
|
Object.defineProperty(exports, "setTable", { enumerable: true, get: function () { return setTable_1.setTable; } });
|
|
47
59
|
Object.defineProperty(exports, "resolveTableAttributes", { enumerable: true, get: function () { return setTable_1.resolveTableAttributes; } });
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The INHERITED-column path for `set-column`.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. set-column originally refused every inherited column outright and told
|
|
5
|
+
* the caller to go change the parent instead, warning in the same breath that doing so
|
|
6
|
+
* "changes the column for EVERY table that extends" it. That advice was wrong, and
|
|
7
|
+
* actively dangerous: it is the destructive option, and ServiceNow has shipped the
|
|
8
|
+
* correct one for years. A child table narrows an inherited column for ITSELF via
|
|
9
|
+
* sys_dictionary_override (mandatory / default / read-only) or sys_documentation (label),
|
|
10
|
+
* touching neither the parent nor any sibling.
|
|
11
|
+
*
|
|
12
|
+
* The refusal was never verified against an instance — the irony being that the verb's
|
|
13
|
+
* own thesis is "do not trust a response you have not read back". Everything below was
|
|
14
|
+
* established live on tenonworkshed, 2026-07-16, by querying real rows:
|
|
15
|
+
*
|
|
16
|
+
* - sys_dictionary_override carries a VALUE column and a paired `<attr>_override`
|
|
17
|
+
* boolean, for mandatory / read_only / default_value (plus calculation, dependent,
|
|
18
|
+
* reference_qual, attributes — outside this verb's five). The boolean is what
|
|
19
|
+
* ACTIVATES the override: a value written without its flag is inert.
|
|
20
|
+
*
|
|
21
|
+
* - There is NO max_length override and NO label override on that table. max_length is
|
|
22
|
+
* physical on the defining table's column, so it genuinely cannot be narrowed per
|
|
23
|
+
* child — that one refusal was right, for a reason the old message never gave.
|
|
24
|
+
*
|
|
25
|
+
* - LABEL is not an override at all; it is a sys_documentation row keyed on the CHILD
|
|
26
|
+
* table. Proof in the platform itself: `problem.short_description` reads "Problem
|
|
27
|
+
* statement" while `task.short_description` — the row that actually defines the
|
|
28
|
+
* column — still reads "Short description". sys_dictionary has no row for
|
|
29
|
+
* problem.short_description whatsoever. OOB ServiceNow does exactly what set-column
|
|
30
|
+
* said was impossible.
|
|
31
|
+
*
|
|
32
|
+
* - `base_table` is the table that DEFINES the column, NOT the immediate parent.
|
|
33
|
+
* Verified against a deep chain: cmdb_ci_endpoint_sharepoint_service extends
|
|
34
|
+
* cmdb_ci_endpoint_inclusion, yet its override rows carry base_table=cmdb_ci.
|
|
35
|
+
*
|
|
36
|
+
* - SCOPE FOLLOWS THE CHILD, not the parent. x_cadso_work_project's overrides of
|
|
37
|
+
* task's columns sit in the x_cadso scope while task is global; problem_task's sit
|
|
38
|
+
* in global. Resolving scope from the parent's dictionary row — the obvious move,
|
|
39
|
+
* and what choices.ts does for its own case — would land a scoped child's override
|
|
40
|
+
* in `global`. That is the exact wrong-scope failure Craftsman's CLAUDE.md calls out.
|
|
41
|
+
*
|
|
42
|
+
* CAPTURE NAMES ARE NOT ANALOGOUS, and this is a trap worth stating plainly. Three
|
|
43
|
+
* record types, three different sys_update_xml naming conventions:
|
|
44
|
+
*
|
|
45
|
+
* sys_dictionary sys_dictionary_<table>_<element>
|
|
46
|
+
* sys_dictionary_override sys_dictionary_override_<SYS_ID> <- keyed by sys_id
|
|
47
|
+
* sys_documentation sys_documentation_<table>_<element>_<language>
|
|
48
|
+
*
|
|
49
|
+
* Deriving the override's name by analogy with the other two produces a query that
|
|
50
|
+
* matches nothing, so a perfectly-captured change would report itself as uncaptured. Both
|
|
51
|
+
* names below were read off real update rows, not inferred.
|
|
52
|
+
*
|
|
53
|
+
* ES6 only, no optional chaining, no `any`.
|
|
54
|
+
*/
|
|
55
|
+
import type { ServiceNowClient } from "../client";
|
|
56
|
+
import type { AttributeChange } from "./setColumn";
|
|
57
|
+
/** The language a label override is written for. ServiceNow keys sys_documentation by
|
|
58
|
+
* language, and the capture name embeds it, so it is explicit rather than implied. */
|
|
59
|
+
export declare var LABEL_LANGUAGE: string;
|
|
60
|
+
export interface OverridableAttribute {
|
|
61
|
+
/** The sys_dictionary_override column that holds the value. */
|
|
62
|
+
field: string;
|
|
63
|
+
/** The boolean that ACTIVATES it. A value written without its flag is inert — the
|
|
64
|
+
* child silently keeps inheriting, which would read as a successful no-op. */
|
|
65
|
+
flag: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The inherited attributes a child can narrow for itself, keyed by the sys_dictionary
|
|
69
|
+
* column set-column diffs on. Anything absent is NOT overridable per-child:
|
|
70
|
+
* - column_label -> sys_documentation instead (see applyLabelOverride)
|
|
71
|
+
* - max_length -> physical on the defining table; no override exists at all
|
|
72
|
+
*/
|
|
73
|
+
export declare var OVERRIDABLE: Record<string, OverridableAttribute>;
|
|
74
|
+
/**
|
|
75
|
+
* Why max_length — alone among set-column's five attributes — cannot be narrowed on a
|
|
76
|
+
* child, and what that means for the caller.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately max_length-specific rather than a general "attribute X is not
|
|
79
|
+
* overridable" helper: of the five, the other four ARE overridable, so a generic branch
|
|
80
|
+
* would be unreachable code speculating about a case that does not exist. If a sixth
|
|
81
|
+
* attribute is ever added to WRITABLE, this needs revisiting — which a dead generic
|
|
82
|
+
* branch would have quietly hidden.
|
|
83
|
+
*
|
|
84
|
+
* Unlike the old blanket refusal this one is true, and it says WHY rather than
|
|
85
|
+
* recommending the destructive alternative as though it were routine.
|
|
86
|
+
*/
|
|
87
|
+
export declare function explainMaxLengthNotOverridable(table: string, column: string, definedOn: string): string;
|
|
88
|
+
/** The scope the override belongs to — resolved from the CHILD table, never the parent.
|
|
89
|
+
* A scoped child overriding a global parent's column must land in the child's scope. */
|
|
90
|
+
export declare function resolveTableScope(client: ServiceNowClient, table: string): Promise<string>;
|
|
91
|
+
/** The child's existing override row for this column, or null. */
|
|
92
|
+
export declare function findOverrideRow(client: ServiceNowClient, table: string, column: string): Promise<Record<string, unknown> | null>;
|
|
93
|
+
/** The child's existing label row for this column, or null. */
|
|
94
|
+
export declare function findLabelRow(client: ServiceNowClient, table: string, column: string): Promise<Record<string, unknown> | null>;
|
|
95
|
+
/**
|
|
96
|
+
* The value this child ACTUALLY sees for an attribute today.
|
|
97
|
+
*
|
|
98
|
+
* An override's value only counts when its flag is on. A row carrying mandatory=true with
|
|
99
|
+
* mandatory_override=false is inert — the child still inherits — so reading the value
|
|
100
|
+
* alone would diff against a number the platform is ignoring, and report `unchanged` for
|
|
101
|
+
* a column that behaves the opposite way.
|
|
102
|
+
*/
|
|
103
|
+
export declare function effectiveValue(field: string, overrideRow: Record<string, unknown> | null, parentRow: Record<string, unknown>, labelRow: Record<string, unknown> | null): string;
|
|
104
|
+
/** One attribute's before -> after.
|
|
105
|
+
*
|
|
106
|
+
* This is setColumn's AttributeChange, imported as a TYPE. setColumn imports values from
|
|
107
|
+
* here, so a value-import back would close a runtime cycle — but `import type` is erased
|
|
108
|
+
* at compile time and cannot. Re-declaring the shape would be a second source of truth
|
|
109
|
+
* for the same three fields, which is worse than the cycle it does not prevent. */
|
|
110
|
+
export type InheritedChange = AttributeChange;
|
|
111
|
+
/**
|
|
112
|
+
* What would actually change for this child, comparing against the value it sees TODAY
|
|
113
|
+
* (its override if one is active, otherwise the inherited value).
|
|
114
|
+
*
|
|
115
|
+
* Note what is deliberately NOT treated as a change: a requested value that already
|
|
116
|
+
* matches the INHERITED one. Writing an override there would pin the child to a value it
|
|
117
|
+
* already has, decoupling it from the parent as an invisible side effect of a request
|
|
118
|
+
* that asked for no such thing. Reporting `unchanged` — and saying the column still
|
|
119
|
+
* tracks the parent — is the honest answer.
|
|
120
|
+
*/
|
|
121
|
+
export declare function diffInherited(writes: Record<string, string>, overrideRow: Record<string, unknown> | null, parentRow: Record<string, unknown>, labelRow: Record<string, unknown> | null): Array<InheritedChange>;
|
|
122
|
+
export interface InheritedWriteParams {
|
|
123
|
+
client: ServiceNowClient;
|
|
124
|
+
table: string;
|
|
125
|
+
column: string;
|
|
126
|
+
/** The table that DEFINES the column — what base_table must be set to. */
|
|
127
|
+
definedOn: string;
|
|
128
|
+
changes: Array<InheritedChange>;
|
|
129
|
+
overrideRow: Record<string, unknown> | null;
|
|
130
|
+
labelRow: Record<string, unknown> | null;
|
|
131
|
+
updateSetSysId: string;
|
|
132
|
+
}
|
|
133
|
+
export interface InheritedWriteResult {
|
|
134
|
+
/** sys_id of the override row touched, "" when no override attribute changed. */
|
|
135
|
+
overrideSysId: string;
|
|
136
|
+
/** sys_id of the label row touched, "" when the label did not change. */
|
|
137
|
+
labelSysId: string;
|
|
138
|
+
/** True only when every written value was READ BACK from the instance and matched. */
|
|
139
|
+
verified: boolean;
|
|
140
|
+
mismatched: Array<string>;
|
|
141
|
+
/** True when every record written was found in the named update set. */
|
|
142
|
+
captured: boolean;
|
|
143
|
+
}
|
|
144
|
+
/** Update-set capture name for an override row — keyed by SYS_ID, unlike sys_dictionary.
|
|
145
|
+
* Read off real update rows on tenonworkshed; deriving it by analogy yields a query that
|
|
146
|
+
* matches nothing and reports a captured change as uncaptured. */
|
|
147
|
+
export declare function overrideUpdateName(overrideSysId: string): string;
|
|
148
|
+
/** Update-set capture name for a label row: sys_documentation_<table>_<element>_<lang>. */
|
|
149
|
+
export declare function labelUpdateName(table: string, column: string): string;
|
|
150
|
+
/**
|
|
151
|
+
* Write the child's overrides, then READ THEM BACK. The whole verb exists because
|
|
152
|
+
* ServiceNow returns 200 for writes it silently discards, and that reasoning does not
|
|
153
|
+
* stop applying just because the target table changed.
|
|
154
|
+
*/
|
|
155
|
+
export declare function applyInheritedWrites(params: InheritedWriteParams): Promise<InheritedWriteResult>;
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The INHERITED-column path for `set-column`.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. set-column originally refused every inherited column outright and told
|
|
6
|
+
* the caller to go change the parent instead, warning in the same breath that doing so
|
|
7
|
+
* "changes the column for EVERY table that extends" it. That advice was wrong, and
|
|
8
|
+
* actively dangerous: it is the destructive option, and ServiceNow has shipped the
|
|
9
|
+
* correct one for years. A child table narrows an inherited column for ITSELF via
|
|
10
|
+
* sys_dictionary_override (mandatory / default / read-only) or sys_documentation (label),
|
|
11
|
+
* touching neither the parent nor any sibling.
|
|
12
|
+
*
|
|
13
|
+
* The refusal was never verified against an instance — the irony being that the verb's
|
|
14
|
+
* own thesis is "do not trust a response you have not read back". Everything below was
|
|
15
|
+
* established live on tenonworkshed, 2026-07-16, by querying real rows:
|
|
16
|
+
*
|
|
17
|
+
* - sys_dictionary_override carries a VALUE column and a paired `<attr>_override`
|
|
18
|
+
* boolean, for mandatory / read_only / default_value (plus calculation, dependent,
|
|
19
|
+
* reference_qual, attributes — outside this verb's five). The boolean is what
|
|
20
|
+
* ACTIVATES the override: a value written without its flag is inert.
|
|
21
|
+
*
|
|
22
|
+
* - There is NO max_length override and NO label override on that table. max_length is
|
|
23
|
+
* physical on the defining table's column, so it genuinely cannot be narrowed per
|
|
24
|
+
* child — that one refusal was right, for a reason the old message never gave.
|
|
25
|
+
*
|
|
26
|
+
* - LABEL is not an override at all; it is a sys_documentation row keyed on the CHILD
|
|
27
|
+
* table. Proof in the platform itself: `problem.short_description` reads "Problem
|
|
28
|
+
* statement" while `task.short_description` — the row that actually defines the
|
|
29
|
+
* column — still reads "Short description". sys_dictionary has no row for
|
|
30
|
+
* problem.short_description whatsoever. OOB ServiceNow does exactly what set-column
|
|
31
|
+
* said was impossible.
|
|
32
|
+
*
|
|
33
|
+
* - `base_table` is the table that DEFINES the column, NOT the immediate parent.
|
|
34
|
+
* Verified against a deep chain: cmdb_ci_endpoint_sharepoint_service extends
|
|
35
|
+
* cmdb_ci_endpoint_inclusion, yet its override rows carry base_table=cmdb_ci.
|
|
36
|
+
*
|
|
37
|
+
* - SCOPE FOLLOWS THE CHILD, not the parent. x_cadso_work_project's overrides of
|
|
38
|
+
* task's columns sit in the x_cadso scope while task is global; problem_task's sit
|
|
39
|
+
* in global. Resolving scope from the parent's dictionary row — the obvious move,
|
|
40
|
+
* and what choices.ts does for its own case — would land a scoped child's override
|
|
41
|
+
* in `global`. That is the exact wrong-scope failure Craftsman's CLAUDE.md calls out.
|
|
42
|
+
*
|
|
43
|
+
* CAPTURE NAMES ARE NOT ANALOGOUS, and this is a trap worth stating plainly. Three
|
|
44
|
+
* record types, three different sys_update_xml naming conventions:
|
|
45
|
+
*
|
|
46
|
+
* sys_dictionary sys_dictionary_<table>_<element>
|
|
47
|
+
* sys_dictionary_override sys_dictionary_override_<SYS_ID> <- keyed by sys_id
|
|
48
|
+
* sys_documentation sys_documentation_<table>_<element>_<language>
|
|
49
|
+
*
|
|
50
|
+
* Deriving the override's name by analogy with the other two produces a query that
|
|
51
|
+
* matches nothing, so a perfectly-captured change would report itself as uncaptured. Both
|
|
52
|
+
* names below were read off real update rows, not inferred.
|
|
53
|
+
*
|
|
54
|
+
* ES6 only, no optional chaining, no `any`.
|
|
55
|
+
*/
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.OVERRIDABLE = exports.LABEL_LANGUAGE = void 0;
|
|
58
|
+
exports.explainMaxLengthNotOverridable = explainMaxLengthNotOverridable;
|
|
59
|
+
exports.resolveTableScope = resolveTableScope;
|
|
60
|
+
exports.findOverrideRow = findOverrideRow;
|
|
61
|
+
exports.findLabelRow = findLabelRow;
|
|
62
|
+
exports.effectiveValue = effectiveValue;
|
|
63
|
+
exports.diffInherited = diffInherited;
|
|
64
|
+
exports.overrideUpdateName = overrideUpdateName;
|
|
65
|
+
exports.labelUpdateName = labelUpdateName;
|
|
66
|
+
exports.applyInheritedWrites = applyInheritedWrites;
|
|
67
|
+
const setField_1 = require("../setField");
|
|
68
|
+
const choices_1 = require("../choices");
|
|
69
|
+
/** The language a label override is written for. ServiceNow keys sys_documentation by
|
|
70
|
+
* language, and the capture name embeds it, so it is explicit rather than implied. */
|
|
71
|
+
exports.LABEL_LANGUAGE = "en";
|
|
72
|
+
/**
|
|
73
|
+
* The inherited attributes a child can narrow for itself, keyed by the sys_dictionary
|
|
74
|
+
* column set-column diffs on. Anything absent is NOT overridable per-child:
|
|
75
|
+
* - column_label -> sys_documentation instead (see applyLabelOverride)
|
|
76
|
+
* - max_length -> physical on the defining table; no override exists at all
|
|
77
|
+
*/
|
|
78
|
+
exports.OVERRIDABLE = {
|
|
79
|
+
mandatory: { field: "mandatory", flag: "mandatory_override" },
|
|
80
|
+
default_value: { field: "default_value", flag: "default_value_override" },
|
|
81
|
+
read_only: { field: "read_only", flag: "read_only_override" },
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Why max_length — alone among set-column's five attributes — cannot be narrowed on a
|
|
85
|
+
* child, and what that means for the caller.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately max_length-specific rather than a general "attribute X is not
|
|
88
|
+
* overridable" helper: of the five, the other four ARE overridable, so a generic branch
|
|
89
|
+
* would be unreachable code speculating about a case that does not exist. If a sixth
|
|
90
|
+
* attribute is ever added to WRITABLE, this needs revisiting — which a dead generic
|
|
91
|
+
* branch would have quietly hidden.
|
|
92
|
+
*
|
|
93
|
+
* Unlike the old blanket refusal this one is true, and it says WHY rather than
|
|
94
|
+
* recommending the destructive alternative as though it were routine.
|
|
95
|
+
*/
|
|
96
|
+
function explainMaxLengthNotOverridable(table, column, definedOn) {
|
|
97
|
+
return ("set-column: refusing to change max_length of '" +
|
|
98
|
+
column +
|
|
99
|
+
"' on '" +
|
|
100
|
+
table +
|
|
101
|
+
"' — the column is INHERITED from '" +
|
|
102
|
+
definedOn +
|
|
103
|
+
"'. max_length is PHYSICAL: it is " +
|
|
104
|
+
"the real database column on '" +
|
|
105
|
+
definedOn +
|
|
106
|
+
"', and sys_dictionary_override has no " +
|
|
107
|
+
"max_length field, so it cannot be narrowed for '" +
|
|
108
|
+
table +
|
|
109
|
+
"' alone. Changing it " +
|
|
110
|
+
"at the source (set-column --table " +
|
|
111
|
+
definedOn +
|
|
112
|
+
" --column " +
|
|
113
|
+
column +
|
|
114
|
+
") resizes " +
|
|
115
|
+
"the column for EVERY table that extends '" +
|
|
116
|
+
definedOn +
|
|
117
|
+
"', so that is a deliberate " +
|
|
118
|
+
"decision and is not made on your behalf. The other attributes (label, mandatory, " +
|
|
119
|
+
"default, read-only) CAN be set on '" +
|
|
120
|
+
table +
|
|
121
|
+
"' alone and are applied as overrides.");
|
|
122
|
+
}
|
|
123
|
+
/** The scope the override belongs to — resolved from the CHILD table, never the parent.
|
|
124
|
+
* A scoped child overriding a global parent's column must land in the child's scope. */
|
|
125
|
+
async function resolveTableScope(client, table) {
|
|
126
|
+
var rows = await client.table.query("sys_db_object", "name=" + (0, choices_1.encodeQueryValue)(table), { limit: 1, fields: ["sys_scope"] });
|
|
127
|
+
if (rows.length === 0)
|
|
128
|
+
return "";
|
|
129
|
+
var scopeSysId = (0, setField_1.fieldToString)(rows[0].sys_scope);
|
|
130
|
+
if (!scopeSysId)
|
|
131
|
+
return "";
|
|
132
|
+
// "global" is already the namespace; sys_scope has no row to resolve it against.
|
|
133
|
+
if (scopeSysId === "global")
|
|
134
|
+
return "global";
|
|
135
|
+
var scopeRows = await client.table.query("sys_scope", "sys_id=" + (0, choices_1.encodeQueryValue)(scopeSysId), { limit: 1, fields: ["scope", "name"] });
|
|
136
|
+
if (scopeRows.length === 0)
|
|
137
|
+
return "";
|
|
138
|
+
return (0, setField_1.fieldToString)(scopeRows[0].scope) || (0, setField_1.fieldToString)(scopeRows[0].name);
|
|
139
|
+
}
|
|
140
|
+
/** The child's existing override row for this column, or null. */
|
|
141
|
+
async function findOverrideRow(client, table, column) {
|
|
142
|
+
var fields = ["sys_id", "name", "element", "base_table"];
|
|
143
|
+
var keys = Object.keys(exports.OVERRIDABLE);
|
|
144
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
145
|
+
fields.push(exports.OVERRIDABLE[keys[i]].field);
|
|
146
|
+
fields.push(exports.OVERRIDABLE[keys[i]].flag);
|
|
147
|
+
}
|
|
148
|
+
var rows = await client.table.query("sys_dictionary_override", "name=" + (0, choices_1.encodeQueryValue)(table) + "^element=" + (0, choices_1.encodeQueryValue)(column), { limit: 1, fields: fields });
|
|
149
|
+
return rows.length > 0 ? rows[0] : null;
|
|
150
|
+
}
|
|
151
|
+
/** The child's existing label row for this column, or null. */
|
|
152
|
+
async function findLabelRow(client, table, column) {
|
|
153
|
+
var rows = await client.table.query("sys_documentation", "name=" +
|
|
154
|
+
(0, choices_1.encodeQueryValue)(table) +
|
|
155
|
+
"^element=" +
|
|
156
|
+
(0, choices_1.encodeQueryValue)(column) +
|
|
157
|
+
"^language=" +
|
|
158
|
+
(0, choices_1.encodeQueryValue)(exports.LABEL_LANGUAGE), { limit: 1, fields: ["sys_id", "label", "language"] });
|
|
159
|
+
return rows.length > 0 ? rows[0] : null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The value this child ACTUALLY sees for an attribute today.
|
|
163
|
+
*
|
|
164
|
+
* An override's value only counts when its flag is on. A row carrying mandatory=true with
|
|
165
|
+
* mandatory_override=false is inert — the child still inherits — so reading the value
|
|
166
|
+
* alone would diff against a number the platform is ignoring, and report `unchanged` for
|
|
167
|
+
* a column that behaves the opposite way.
|
|
168
|
+
*/
|
|
169
|
+
function effectiveValue(field, overrideRow, parentRow, labelRow) {
|
|
170
|
+
// The label is not an override; it is the child's own sys_documentation row. When the
|
|
171
|
+
// child has none it simply shows the defining table's label, which sys_dictionary
|
|
172
|
+
// already surfaces as column_label on the parent row.
|
|
173
|
+
if (field === "column_label") {
|
|
174
|
+
if (labelRow)
|
|
175
|
+
return (0, setField_1.fieldToString)(labelRow.label);
|
|
176
|
+
return (0, setField_1.fieldToString)(parentRow.column_label);
|
|
177
|
+
}
|
|
178
|
+
var spec = exports.OVERRIDABLE[field];
|
|
179
|
+
if (spec && overrideRow) {
|
|
180
|
+
var flag = (0, setField_1.fieldToString)(overrideRow[spec.flag]);
|
|
181
|
+
if (flag === "true")
|
|
182
|
+
return (0, setField_1.fieldToString)(overrideRow[spec.field]);
|
|
183
|
+
}
|
|
184
|
+
return (0, setField_1.fieldToString)(parentRow[field]);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* What would actually change for this child, comparing against the value it sees TODAY
|
|
188
|
+
* (its override if one is active, otherwise the inherited value).
|
|
189
|
+
*
|
|
190
|
+
* Note what is deliberately NOT treated as a change: a requested value that already
|
|
191
|
+
* matches the INHERITED one. Writing an override there would pin the child to a value it
|
|
192
|
+
* already has, decoupling it from the parent as an invisible side effect of a request
|
|
193
|
+
* that asked for no such thing. Reporting `unchanged` — and saying the column still
|
|
194
|
+
* tracks the parent — is the honest answer.
|
|
195
|
+
*/
|
|
196
|
+
function diffInherited(writes, overrideRow, parentRow, labelRow) {
|
|
197
|
+
var changes = [];
|
|
198
|
+
var fields = Object.keys(writes);
|
|
199
|
+
for (var i = 0; i < fields.length; i += 1) {
|
|
200
|
+
var field = fields[i];
|
|
201
|
+
var from = effectiveValue(field, overrideRow, parentRow, labelRow);
|
|
202
|
+
if (from !== writes[field]) {
|
|
203
|
+
changes.push({ attribute: field, from: from, to: writes[field] });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return changes;
|
|
207
|
+
}
|
|
208
|
+
/** Update-set capture name for an override row — keyed by SYS_ID, unlike sys_dictionary.
|
|
209
|
+
* Read off real update rows on tenonworkshed; deriving it by analogy yields a query that
|
|
210
|
+
* matches nothing and reports a captured change as uncaptured. */
|
|
211
|
+
function overrideUpdateName(overrideSysId) {
|
|
212
|
+
return "sys_dictionary_override_" + overrideSysId;
|
|
213
|
+
}
|
|
214
|
+
/** Update-set capture name for a label row: sys_documentation_<table>_<element>_<lang>. */
|
|
215
|
+
function labelUpdateName(table, column) {
|
|
216
|
+
return "sys_documentation_" + table + "_" + column + "_" + exports.LABEL_LANGUAGE;
|
|
217
|
+
}
|
|
218
|
+
async function isCaptured(client, updateSetSysId, name) {
|
|
219
|
+
var rows = await client.table.query("sys_update_xml", "update_set=" +
|
|
220
|
+
(0, choices_1.encodeQueryValue)(updateSetSysId) +
|
|
221
|
+
"^name=" +
|
|
222
|
+
(0, choices_1.encodeQueryValue)(name), { limit: 1, fields: ["sys_id"] });
|
|
223
|
+
return rows.length > 0;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Write the child's overrides, then READ THEM BACK. The whole verb exists because
|
|
227
|
+
* ServiceNow returns 200 for writes it silently discards, and that reasoning does not
|
|
228
|
+
* stop applying just because the target table changed.
|
|
229
|
+
*/
|
|
230
|
+
async function applyInheritedWrites(params) {
|
|
231
|
+
var client = params.client;
|
|
232
|
+
var overrideChanges = params.changes.filter(function (c) {
|
|
233
|
+
return Boolean(exports.OVERRIDABLE[c.attribute]);
|
|
234
|
+
});
|
|
235
|
+
var labelChange = params.changes.filter(function (c) {
|
|
236
|
+
return c.attribute === "column_label";
|
|
237
|
+
})[0];
|
|
238
|
+
// The sys_ids of any PRE-EXISTING rows — used only to TARGET an update, never reported.
|
|
239
|
+
// A label-only call must not claim it touched the override row that happened to already
|
|
240
|
+
// exist, and an override-only call must not claim it touched a pre-existing label row.
|
|
241
|
+
var existingOverrideSysId = params.overrideRow
|
|
242
|
+
? (0, setField_1.fieldToString)(params.overrideRow.sys_id)
|
|
243
|
+
: "";
|
|
244
|
+
var existingLabelSysId = params.labelRow
|
|
245
|
+
? (0, setField_1.fieldToString)(params.labelRow.sys_id)
|
|
246
|
+
: "";
|
|
247
|
+
// What this call actually WROTE. Stays "" unless the matching branch runs, so a returned
|
|
248
|
+
// sys_id names a row THIS call touched — honouring the InheritedWriteResult docstrings,
|
|
249
|
+
// which promise "" when the attribute did not change.
|
|
250
|
+
var overrideSysId = "";
|
|
251
|
+
var labelSysId = "";
|
|
252
|
+
// Scope follows the CHILD — taking it from the parent would drop a scoped child's
|
|
253
|
+
// override into global. Resolved at most once and only when a record is actually
|
|
254
|
+
// created: a mixed label+mandatory request needs the same answer twice, and an update
|
|
255
|
+
// to existing rows needs it not at all.
|
|
256
|
+
var scopeMemo = null;
|
|
257
|
+
async function childScope() {
|
|
258
|
+
if (scopeMemo === null) {
|
|
259
|
+
scopeMemo = await resolveTableScope(client, params.table);
|
|
260
|
+
}
|
|
261
|
+
return scopeMemo ? scopeMemo : undefined;
|
|
262
|
+
}
|
|
263
|
+
// --- sys_dictionary_override -------------------------------------------------------
|
|
264
|
+
if (overrideChanges.length > 0) {
|
|
265
|
+
var overrideFields = {};
|
|
266
|
+
for (var i = 0; i < overrideChanges.length; i += 1) {
|
|
267
|
+
var spec = exports.OVERRIDABLE[overrideChanges[i].attribute];
|
|
268
|
+
overrideFields[spec.field] = overrideChanges[i].to;
|
|
269
|
+
// The flag is what makes the value count. Writing the value alone leaves the child
|
|
270
|
+
// inheriting while the row claims otherwise.
|
|
271
|
+
overrideFields[spec.flag] = "true";
|
|
272
|
+
}
|
|
273
|
+
if (existingOverrideSysId) {
|
|
274
|
+
await client.claude.pushWithUpdateSet({
|
|
275
|
+
update_set_sys_id: params.updateSetSysId,
|
|
276
|
+
table: "sys_dictionary_override",
|
|
277
|
+
record_sys_id: existingOverrideSysId,
|
|
278
|
+
fields: overrideFields,
|
|
279
|
+
});
|
|
280
|
+
overrideSysId = existingOverrideSysId;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
overrideFields.name = params.table;
|
|
284
|
+
overrideFields.element = params.column;
|
|
285
|
+
// base_table is the DEFINING table, not the immediate parent (verified live).
|
|
286
|
+
overrideFields.base_table = params.definedOn;
|
|
287
|
+
var created = await client.claude.createRecord({
|
|
288
|
+
table: "sys_dictionary_override",
|
|
289
|
+
fields: overrideFields,
|
|
290
|
+
scope: await childScope(),
|
|
291
|
+
update_set_sys_id: params.updateSetSysId,
|
|
292
|
+
});
|
|
293
|
+
overrideSysId = (0, setField_1.fieldToString)(created.sys_id);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// --- sys_documentation (label) -----------------------------------------------------
|
|
297
|
+
if (labelChange) {
|
|
298
|
+
if (existingLabelSysId) {
|
|
299
|
+
await client.claude.pushWithUpdateSet({
|
|
300
|
+
update_set_sys_id: params.updateSetSysId,
|
|
301
|
+
table: "sys_documentation",
|
|
302
|
+
record_sys_id: existingLabelSysId,
|
|
303
|
+
fields: { label: labelChange.to },
|
|
304
|
+
});
|
|
305
|
+
labelSysId = existingLabelSysId;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
var createdLabel = await client.claude.createRecord({
|
|
309
|
+
table: "sys_documentation",
|
|
310
|
+
fields: {
|
|
311
|
+
name: params.table,
|
|
312
|
+
element: params.column,
|
|
313
|
+
label: labelChange.to,
|
|
314
|
+
language: exports.LABEL_LANGUAGE,
|
|
315
|
+
},
|
|
316
|
+
scope: await childScope(),
|
|
317
|
+
update_set_sys_id: params.updateSetSysId,
|
|
318
|
+
});
|
|
319
|
+
labelSysId = (0, setField_1.fieldToString)(createdLabel.sys_id);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// --- read back ---------------------------------------------------------------------
|
|
323
|
+
var mismatched = [];
|
|
324
|
+
if (overrideChanges.length > 0) {
|
|
325
|
+
var afterOverride = await findOverrideRow(client, params.table, params.column);
|
|
326
|
+
if (!afterOverride) {
|
|
327
|
+
mismatched.push("no sys_dictionary_override row for " +
|
|
328
|
+
params.table +
|
|
329
|
+
"." +
|
|
330
|
+
params.column +
|
|
331
|
+
" could be read back after the write");
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
for (var j = 0; j < overrideChanges.length; j += 1) {
|
|
335
|
+
var oSpec = exports.OVERRIDABLE[overrideChanges[j].attribute];
|
|
336
|
+
var got = (0, setField_1.fieldToString)(afterOverride[oSpec.field]);
|
|
337
|
+
var gotFlag = (0, setField_1.fieldToString)(afterOverride[oSpec.flag]);
|
|
338
|
+
if (got !== overrideChanges[j].to) {
|
|
339
|
+
mismatched.push(overrideChanges[j].attribute +
|
|
340
|
+
" reads back as '" +
|
|
341
|
+
got +
|
|
342
|
+
"', not '" +
|
|
343
|
+
overrideChanges[j].to +
|
|
344
|
+
"'");
|
|
345
|
+
}
|
|
346
|
+
else if (gotFlag !== "true") {
|
|
347
|
+
// The value landed but the override is not switched on, so the child still
|
|
348
|
+
// inherits. Nothing visibly failed, and the column does not behave as asked.
|
|
349
|
+
mismatched.push(oSpec.flag +
|
|
350
|
+
" reads back as '" +
|
|
351
|
+
gotFlag +
|
|
352
|
+
"', so the " +
|
|
353
|
+
overrideChanges[j].attribute +
|
|
354
|
+
" override is INERT and " +
|
|
355
|
+
params.table +
|
|
356
|
+
" still inherits from " +
|
|
357
|
+
params.definedOn);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (labelChange) {
|
|
363
|
+
var afterLabel = await findLabelRow(client, params.table, params.column);
|
|
364
|
+
var gotLabel = afterLabel ? (0, setField_1.fieldToString)(afterLabel.label) : "";
|
|
365
|
+
if (gotLabel !== labelChange.to) {
|
|
366
|
+
mismatched.push("label reads back as '" + gotLabel + "', not '" + labelChange.to + "'");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// --- capture -----------------------------------------------------------------------
|
|
370
|
+
var captured = true;
|
|
371
|
+
if (overrideChanges.length > 0 && overrideSysId) {
|
|
372
|
+
captured =
|
|
373
|
+
captured &&
|
|
374
|
+
(await isCaptured(client, params.updateSetSysId, overrideUpdateName(overrideSysId)));
|
|
375
|
+
}
|
|
376
|
+
if (labelChange) {
|
|
377
|
+
captured =
|
|
378
|
+
captured &&
|
|
379
|
+
(await isCaptured(client, params.updateSetSysId, labelUpdateName(params.table, params.column)));
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
overrideSysId: overrideSysId,
|
|
383
|
+
labelSysId: labelSysId,
|
|
384
|
+
verified: mismatched.length === 0,
|
|
385
|
+
mismatched: mismatched,
|
|
386
|
+
captured: captured,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
@@ -50,6 +50,19 @@
|
|
|
50
50
|
* do nothing. That is why every write is read back and compared, and why nothing is ever
|
|
51
51
|
* reported as applied on the strength of a status code.
|
|
52
52
|
*
|
|
53
|
+
* INHERITED COLUMNS. On an extended table the column is defined on an ancestor, not here.
|
|
54
|
+
* That is not an error and not a dead end: mandatory / default / read_only are narrowed
|
|
55
|
+
* for THIS table alone via sys_dictionary_override, and label via sys_documentation,
|
|
56
|
+
* leaving the ancestor and every sibling untouched. See overrideColumn.ts, which carries
|
|
57
|
+
* the live-verified detail. max_length is the one real exception — it is the ancestor's
|
|
58
|
+
* physical column and has no per-child override, so it is refused with the reason.
|
|
59
|
+
*
|
|
60
|
+
* This verb originally refused every inherited column and told the caller to go edit the
|
|
61
|
+
* ancestor "since that changes it for EVERY table that extends" it. That was the
|
|
62
|
+
* destructive option offered as the only one, and it was never checked against an
|
|
63
|
+
* instance — a claim about ServiceNow trusted on reasoning alone, in the one file whose
|
|
64
|
+
* whole argument is that ServiceNow must be read back rather than believed.
|
|
65
|
+
*
|
|
53
66
|
* Fields are taken from a strict ALLOWLIST, not an open map — an unbounded write to
|
|
54
67
|
* sys_dictionary lets a caller quietly corrupt the schema.
|
|
55
68
|
*
|
|
@@ -95,8 +108,21 @@ export interface SetColumnResult {
|
|
|
95
108
|
status: "dry-run" | "applied" | "unchanged" | "failed";
|
|
96
109
|
table: string;
|
|
97
110
|
column: string;
|
|
98
|
-
/** sys_id of the sys_dictionary row.
|
|
111
|
+
/** sys_id of the sys_dictionary row that DEFINES the column. On an inherited column
|
|
112
|
+
* this is the ancestor's row — the child has none — so it is not what was written;
|
|
113
|
+
* see overrideSysId / labelSysId for that. */
|
|
99
114
|
columnSysId: string;
|
|
115
|
+
/** How the change was made. "dictionary" — written straight to the column's own
|
|
116
|
+
* sys_dictionary row. "override" — the column is inherited, so it was narrowed for
|
|
117
|
+
* THIS table alone via sys_dictionary_override / sys_documentation, leaving the
|
|
118
|
+
* defining table and every sibling untouched. */
|
|
119
|
+
via: "dictionary" | "override";
|
|
120
|
+
/** The ancestor that defines the column, when it is inherited; "" when it is local. */
|
|
121
|
+
definedOn: string;
|
|
122
|
+
/** sys_id of the sys_dictionary_override row written, when via === "override". */
|
|
123
|
+
overrideSysId?: string;
|
|
124
|
+
/** sys_id of the sys_documentation row written, when a label override was applied. */
|
|
125
|
+
labelSysId?: string;
|
|
100
126
|
updateSetSysId: string;
|
|
101
127
|
/** The attributes that differed and were written (empty when nothing changed). */
|
|
102
128
|
changes: Array<AttributeChange>;
|
package/dist/table/setColumn.js
CHANGED
|
@@ -51,6 +51,19 @@
|
|
|
51
51
|
* do nothing. That is why every write is read back and compared, and why nothing is ever
|
|
52
52
|
* reported as applied on the strength of a status code.
|
|
53
53
|
*
|
|
54
|
+
* INHERITED COLUMNS. On an extended table the column is defined on an ancestor, not here.
|
|
55
|
+
* That is not an error and not a dead end: mandatory / default / read_only are narrowed
|
|
56
|
+
* for THIS table alone via sys_dictionary_override, and label via sys_documentation,
|
|
57
|
+
* leaving the ancestor and every sibling untouched. See overrideColumn.ts, which carries
|
|
58
|
+
* the live-verified detail. max_length is the one real exception — it is the ancestor's
|
|
59
|
+
* physical column and has no per-child override, so it is refused with the reason.
|
|
60
|
+
*
|
|
61
|
+
* This verb originally refused every inherited column and told the caller to go edit the
|
|
62
|
+
* ancestor "since that changes it for EVERY table that extends" it. That was the
|
|
63
|
+
* destructive option offered as the only one, and it was never checked against an
|
|
64
|
+
* instance — a claim about ServiceNow trusted on reasoning alone, in the one file whose
|
|
65
|
+
* whole argument is that ServiceNow must be read back rather than believed.
|
|
66
|
+
*
|
|
54
67
|
* Fields are taken from a strict ALLOWLIST, not an open map — an unbounded write to
|
|
55
68
|
* sys_dictionary lets a caller quietly corrupt the schema.
|
|
56
69
|
*
|
|
@@ -66,6 +79,10 @@ const setField_1 = require("../setField");
|
|
|
66
79
|
// through this first. A stray "^" or "=" does not error — it silently changes what the
|
|
67
80
|
// query MEANS, which is the worst kind of bug to ship into a schema tool.
|
|
68
81
|
const choices_1 = require("../choices");
|
|
82
|
+
// The inherited-column path. An extended table's columns live on an ancestor's
|
|
83
|
+
// dictionary rows, and ServiceNow's answer for narrowing one per-child is
|
|
84
|
+
// sys_dictionary_override / sys_documentation — not editing the ancestor.
|
|
85
|
+
const overrideColumn_1 = require("./overrideColumn");
|
|
69
86
|
/** How far up a super_class chain to look for an inherited column before giving up. */
|
|
70
87
|
var MAX_INHERITANCE_DEPTH = 20;
|
|
71
88
|
/**
|
|
@@ -157,8 +174,12 @@ function resolveAttributes(attributes) {
|
|
|
157
174
|
* child's — a child of x_cadso_journey_flow carries only a couple of dictionary rows of
|
|
158
175
|
* its own while every real column is inherited. So a plain name+element lookup against
|
|
159
176
|
* the child finds nothing, even though the column is plainly there on the form. Walking
|
|
160
|
-
* super_class
|
|
161
|
-
*
|
|
177
|
+
* super_class finds the row that really defines it.
|
|
178
|
+
*
|
|
179
|
+
* That answer used to end the story: set-column reported "it lives on <parent>, go change
|
|
180
|
+
* it there" and stopped. It now ROUTES instead — the defining table is where the column's
|
|
181
|
+
* inherited values are read from for the diff, and what `base_table` is set to on the
|
|
182
|
+
* override that narrows it for the child. See overrideColumn.ts.
|
|
162
183
|
*/
|
|
163
184
|
async function findDefiningTable(client, table, column) {
|
|
164
185
|
var current = table;
|
|
@@ -185,31 +206,31 @@ async function findDefiningTable(client, table, column) {
|
|
|
185
206
|
}
|
|
186
207
|
return "";
|
|
187
208
|
}
|
|
188
|
-
/**
|
|
189
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Find the column's dictionary row — on the table itself, or on the ancestor that
|
|
211
|
+
* defines it — or throw a message that says what to check.
|
|
212
|
+
*
|
|
213
|
+
* An inherited column is NOT an error. It is the normal shape of an extended table, and
|
|
214
|
+
* the caller's request ("make this column mandatory on this table") is answerable
|
|
215
|
+
* exactly as asked, by overriding it for the child. Resolution just reports where the
|
|
216
|
+
* definition lives; setColumn decides what to write.
|
|
217
|
+
*/
|
|
218
|
+
async function resolveColumn(client, table, column, fields) {
|
|
190
219
|
var rows = await client.table.query("sys_dictionary", "name=" + (0, choices_1.encodeQueryValue)(table) + "^element=" + (0, choices_1.encodeQueryValue)(column), { limit: 1, fields: fields });
|
|
191
220
|
if (rows.length > 0)
|
|
192
|
-
return rows[0];
|
|
221
|
+
return { row: rows[0], definedOn: "" };
|
|
193
222
|
// Before declaring the column missing, check whether it is simply inherited. Saying
|
|
194
223
|
// "no such column" about a column the caller can see on the form sends them hunting
|
|
195
224
|
// for a typo that is not there.
|
|
196
225
|
var owner = await findDefiningTable(client, table, column);
|
|
197
226
|
if (owner) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
owner
|
|
204
|
-
|
|
205
|
-
owner +
|
|
206
|
-
" --column " +
|
|
207
|
-
column +
|
|
208
|
-
". Note that doing so changes the column for EVERY table that extends " +
|
|
209
|
-
owner +
|
|
210
|
-
", not just " +
|
|
211
|
-
table +
|
|
212
|
-
" — which is why this is not done implicitly on your behalf.");
|
|
227
|
+
var ownerRows = await client.table.query("sys_dictionary", "name=" +
|
|
228
|
+
(0, choices_1.encodeQueryValue)(owner) +
|
|
229
|
+
"^element=" +
|
|
230
|
+
(0, choices_1.encodeQueryValue)(column), { limit: 1, fields: fields });
|
|
231
|
+
if (ownerRows.length > 0) {
|
|
232
|
+
return { row: ownerRows[0], definedOn: owner };
|
|
233
|
+
}
|
|
213
234
|
}
|
|
214
235
|
throw new Error("set-column: no column '" +
|
|
215
236
|
column +
|
|
@@ -357,9 +378,15 @@ async function setColumn(params) {
|
|
|
357
378
|
var table = String(params.table).trim();
|
|
358
379
|
var column = String(params.column).trim();
|
|
359
380
|
var readFields = ["sys_id", "element", "internal_type"].concat(targets);
|
|
360
|
-
var
|
|
381
|
+
var resolved = await resolveColumn(params.client, table, column, readFields);
|
|
382
|
+
var row = resolved.row;
|
|
361
383
|
// max_length is meaningless on a type that has no length. ServiceNow would take the
|
|
362
384
|
// write and ignore it (a fourth silent no-op), so refuse it here where we can say why.
|
|
385
|
+
//
|
|
386
|
+
// This runs BEFORE the inherited branch on purpose. The type is a property of the
|
|
387
|
+
// COLUMN, so it is wrong wherever the column is defined — and the inherited refusal
|
|
388
|
+
// says "change it at the source instead", which for a lengthless type would send the
|
|
389
|
+
// caller to the parent to attempt something that cannot work there either.
|
|
363
390
|
if (writes.max_length !== undefined) {
|
|
364
391
|
var columnType = (0, setField_1.fieldToString)(row.internal_type);
|
|
365
392
|
if (LENGTHLESS_TYPES.indexOf(columnType) !== -1) {
|
|
@@ -371,6 +398,17 @@ async function setColumn(params) {
|
|
|
371
398
|
"ServiceNow would accept the write and ignore it. Drop --max-length.");
|
|
372
399
|
}
|
|
373
400
|
}
|
|
401
|
+
// The column is INHERITED. It is narrowed for this table alone via an override — never
|
|
402
|
+
// by editing the ancestor, which would change it for every sibling too.
|
|
403
|
+
if (resolved.definedOn) {
|
|
404
|
+
return await setInheritedColumn(params, {
|
|
405
|
+
table: table,
|
|
406
|
+
column: column,
|
|
407
|
+
writes: writes,
|
|
408
|
+
parentRow: row,
|
|
409
|
+
definedOn: resolved.definedOn,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
374
412
|
var columnSysId = (0, setField_1.fieldToString)(row.sys_id);
|
|
375
413
|
// Diff against what the instance actually stores. Only a genuine difference is
|
|
376
414
|
// written: ServiceNow fires the physical ALTER on a CHANGE, and a same-value write
|
|
@@ -428,6 +466,8 @@ async function setColumn(params) {
|
|
|
428
466
|
table: table,
|
|
429
467
|
column: column,
|
|
430
468
|
columnSysId: columnSysId,
|
|
469
|
+
via: "dictionary",
|
|
470
|
+
definedOn: "",
|
|
431
471
|
updateSetSysId: params.updateSetSysId ? params.updateSetSysId : "",
|
|
432
472
|
changes: changes,
|
|
433
473
|
verified: false,
|
|
@@ -479,6 +519,8 @@ async function setColumn(params) {
|
|
|
479
519
|
table: table,
|
|
480
520
|
column: column,
|
|
481
521
|
columnSysId: columnSysId,
|
|
522
|
+
via: "dictionary",
|
|
523
|
+
definedOn: "",
|
|
482
524
|
updateSetSysId: updateSetSysId,
|
|
483
525
|
changes: [],
|
|
484
526
|
verified: true,
|
|
@@ -515,6 +557,8 @@ async function setColumn(params) {
|
|
|
515
557
|
table: table,
|
|
516
558
|
column: column,
|
|
517
559
|
columnSysId: columnSysId,
|
|
560
|
+
via: "dictionary",
|
|
561
|
+
definedOn: "",
|
|
518
562
|
updateSetSysId: updateSetSysId,
|
|
519
563
|
changes: changes,
|
|
520
564
|
verified: false,
|
|
@@ -538,7 +582,7 @@ async function setColumn(params) {
|
|
|
538
582
|
var after;
|
|
539
583
|
var captured;
|
|
540
584
|
try {
|
|
541
|
-
after = await
|
|
585
|
+
after = (await resolveColumn(params.client, table, column, readFields)).row;
|
|
542
586
|
captured = await assertCaptured(params.client, table, column, updateSetSysId);
|
|
543
587
|
}
|
|
544
588
|
catch (e) {
|
|
@@ -547,6 +591,8 @@ async function setColumn(params) {
|
|
|
547
591
|
table: table,
|
|
548
592
|
column: column,
|
|
549
593
|
columnSysId: columnSysId,
|
|
594
|
+
via: "dictionary",
|
|
595
|
+
definedOn: "",
|
|
550
596
|
updateSetSysId: updateSetSysId,
|
|
551
597
|
changes: changes,
|
|
552
598
|
verified: false,
|
|
@@ -577,6 +623,8 @@ async function setColumn(params) {
|
|
|
577
623
|
table: table,
|
|
578
624
|
column: column,
|
|
579
625
|
columnSysId: columnSysId,
|
|
626
|
+
via: "dictionary",
|
|
627
|
+
definedOn: "",
|
|
580
628
|
updateSetSysId: updateSetSysId,
|
|
581
629
|
changes: changes,
|
|
582
630
|
verified: false,
|
|
@@ -603,6 +651,8 @@ async function setColumn(params) {
|
|
|
603
651
|
table: table,
|
|
604
652
|
column: column,
|
|
605
653
|
columnSysId: columnSysId,
|
|
654
|
+
via: "dictionary",
|
|
655
|
+
definedOn: "",
|
|
606
656
|
updateSetSysId: updateSetSysId,
|
|
607
657
|
changes: changes,
|
|
608
658
|
verified: true,
|
|
@@ -629,6 +679,201 @@ async function setColumn(params) {
|
|
|
629
679
|
"promoted. Check the update set is in progress and in the column's scope.",
|
|
630
680
|
};
|
|
631
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Set an INHERITED column's attributes for this table alone.
|
|
684
|
+
*
|
|
685
|
+
* The caller asked to change a column on a child table. ServiceNow's answer to that is an
|
|
686
|
+
* override on the child — not an edit to the ancestor, which would silently change the
|
|
687
|
+
* column for every other table extending it. Four of the five attributes set-column
|
|
688
|
+
* supports can be narrowed this way; max_length cannot, because it is the ancestor's
|
|
689
|
+
* physical column, and that is the one case where "change it at the source" is the honest
|
|
690
|
+
* answer (given with its blast radius spelled out, rather than as a casual suggestion).
|
|
691
|
+
*/
|
|
692
|
+
async function setInheritedColumn(params, ctx) {
|
|
693
|
+
var columnSysId = (0, setField_1.fieldToString)(ctx.parentRow.sys_id);
|
|
694
|
+
// Refuse before touching anything else, so a dry-run fails identically to a live run.
|
|
695
|
+
if (ctx.writes.max_length !== undefined) {
|
|
696
|
+
throw new Error((0, overrideColumn_1.explainMaxLengthNotOverridable)(ctx.table, ctx.column, ctx.definedOn));
|
|
697
|
+
}
|
|
698
|
+
var overrideRow = await (0, overrideColumn_1.findOverrideRow)(params.client, ctx.table, ctx.column);
|
|
699
|
+
var labelRow = await (0, overrideColumn_1.findLabelRow)(params.client, ctx.table, ctx.column);
|
|
700
|
+
var changes = (0, overrideColumn_1.diffInherited)(ctx.writes, overrideRow, ctx.parentRow, labelRow);
|
|
701
|
+
var base = {
|
|
702
|
+
table: ctx.table,
|
|
703
|
+
column: ctx.column,
|
|
704
|
+
columnSysId: columnSysId,
|
|
705
|
+
via: "override",
|
|
706
|
+
definedOn: ctx.definedOn,
|
|
707
|
+
};
|
|
708
|
+
if (params.dryRun) {
|
|
709
|
+
return Object.assign({}, base, {
|
|
710
|
+
status: "dry-run",
|
|
711
|
+
updateSetSysId: params.updateSetSysId ? params.updateSetSysId : "",
|
|
712
|
+
changes: changes,
|
|
713
|
+
verified: false,
|
|
714
|
+
capturedInUpdateSet: false,
|
|
715
|
+
note: changes.length === 0
|
|
716
|
+
? "dry-run: no write. " +
|
|
717
|
+
ctx.column +
|
|
718
|
+
" is inherited from " +
|
|
719
|
+
ctx.definedOn +
|
|
720
|
+
" and already presents every requested value on " +
|
|
721
|
+
ctx.table +
|
|
722
|
+
" — nothing would change."
|
|
723
|
+
: "dry-run: no write. " +
|
|
724
|
+
ctx.column +
|
|
725
|
+
" is inherited from " +
|
|
726
|
+
ctx.definedOn +
|
|
727
|
+
"; would override " +
|
|
728
|
+
describeChanges(changes) +
|
|
729
|
+
" for " +
|
|
730
|
+
ctx.table +
|
|
731
|
+
" ALONE (" +
|
|
732
|
+
describeTargets(changes) +
|
|
733
|
+
"), leaving " +
|
|
734
|
+
ctx.definedOn +
|
|
735
|
+
" and its other children untouched. Captured into update set " +
|
|
736
|
+
(params.updateSetSysId
|
|
737
|
+
? params.updateSetSysId
|
|
738
|
+
: "(none provided)") +
|
|
739
|
+
".",
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
if (!params.updateSetSysId || !String(params.updateSetSysId).trim()) {
|
|
743
|
+
throw new Error("set-column: --update-set <sys_id> is required so the schema change is captured " +
|
|
744
|
+
"and can be promoted. An uncaptured change exists only on this instance.");
|
|
745
|
+
}
|
|
746
|
+
var updateSetSysId = String(params.updateSetSysId).trim();
|
|
747
|
+
await assertUpdateSetOpen(params.client, updateSetSysId);
|
|
748
|
+
// Nothing differs from what the child already sees. Note WHY no override was written:
|
|
749
|
+
// pinning a value the column already inherits would decouple it from the ancestor as an
|
|
750
|
+
// invisible side effect of a request that never asked for that.
|
|
751
|
+
if (changes.length === 0) {
|
|
752
|
+
return Object.assign({}, base, {
|
|
753
|
+
status: "unchanged",
|
|
754
|
+
updateSetSysId: updateSetSysId,
|
|
755
|
+
changes: [],
|
|
756
|
+
verified: true,
|
|
757
|
+
capturedInUpdateSet: false,
|
|
758
|
+
note: ctx.table +
|
|
759
|
+
"." +
|
|
760
|
+
ctx.column +
|
|
761
|
+
" already presents every requested value — nothing written. The column is " +
|
|
762
|
+
"inherited from " +
|
|
763
|
+
ctx.definedOn +
|
|
764
|
+
" and no override was created, so it still TRACKS " +
|
|
765
|
+
ctx.definedOn +
|
|
766
|
+
": a later change there will follow through to " +
|
|
767
|
+
ctx.table +
|
|
768
|
+
".",
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
var written;
|
|
772
|
+
try {
|
|
773
|
+
written = await (0, overrideColumn_1.applyInheritedWrites)({
|
|
774
|
+
client: params.client,
|
|
775
|
+
table: ctx.table,
|
|
776
|
+
column: ctx.column,
|
|
777
|
+
definedOn: ctx.definedOn,
|
|
778
|
+
changes: changes,
|
|
779
|
+
overrideRow: overrideRow,
|
|
780
|
+
labelRow: labelRow,
|
|
781
|
+
updateSetSysId: updateSetSysId,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
catch (e) {
|
|
785
|
+
return Object.assign({}, base, {
|
|
786
|
+
status: "failed",
|
|
787
|
+
updateSetSysId: updateSetSysId,
|
|
788
|
+
changes: changes,
|
|
789
|
+
verified: false,
|
|
790
|
+
capturedInUpdateSet: false,
|
|
791
|
+
note: "the override write for " +
|
|
792
|
+
ctx.table +
|
|
793
|
+
"." +
|
|
794
|
+
ctx.column +
|
|
795
|
+
" failed: " +
|
|
796
|
+
(e && e.message ? e.message : String(e)) +
|
|
797
|
+
". It is NOT known whether it landed — read sys_dictionary_override for name=" +
|
|
798
|
+
ctx.table +
|
|
799
|
+
"^element=" +
|
|
800
|
+
ctx.column +
|
|
801
|
+
" on the instance before retrying.",
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
if (!written.verified) {
|
|
805
|
+
return Object.assign({}, base, {
|
|
806
|
+
status: "failed",
|
|
807
|
+
updateSetSysId: updateSetSysId,
|
|
808
|
+
overrideSysId: written.overrideSysId,
|
|
809
|
+
labelSysId: written.labelSysId,
|
|
810
|
+
changes: changes,
|
|
811
|
+
verified: false,
|
|
812
|
+
capturedInUpdateSet: written.captured,
|
|
813
|
+
note: "the override write returned success but the instance does NOT reflect it: " +
|
|
814
|
+
written.mismatched.join("; ") +
|
|
815
|
+
". Treat " +
|
|
816
|
+
ctx.table +
|
|
817
|
+
"." +
|
|
818
|
+
ctx.column +
|
|
819
|
+
" as NOT overridden and reconcile it on the instance.",
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return Object.assign({}, base, {
|
|
823
|
+
status: "applied",
|
|
824
|
+
updateSetSysId: updateSetSysId,
|
|
825
|
+
overrideSysId: written.overrideSysId,
|
|
826
|
+
labelSysId: written.labelSysId,
|
|
827
|
+
changes: changes,
|
|
828
|
+
verified: true,
|
|
829
|
+
capturedInUpdateSet: written.captured,
|
|
830
|
+
note: written.captured
|
|
831
|
+
? "Overrode " +
|
|
832
|
+
describeChanges(changes) +
|
|
833
|
+
" for " +
|
|
834
|
+
ctx.table +
|
|
835
|
+
"." +
|
|
836
|
+
ctx.column +
|
|
837
|
+
" — inherited from " +
|
|
838
|
+
ctx.definedOn +
|
|
839
|
+
", now narrowed for " +
|
|
840
|
+
ctx.table +
|
|
841
|
+
" ALONE (" +
|
|
842
|
+
describeTargets(changes) +
|
|
843
|
+
"); " +
|
|
844
|
+
ctx.definedOn +
|
|
845
|
+
" and its other children are unchanged. Verified by read-back and captured in " +
|
|
846
|
+
"update set " +
|
|
847
|
+
updateSetSysId +
|
|
848
|
+
"."
|
|
849
|
+
: "Overrode " +
|
|
850
|
+
describeChanges(changes) +
|
|
851
|
+
" for " +
|
|
852
|
+
ctx.table +
|
|
853
|
+
"." +
|
|
854
|
+
ctx.column +
|
|
855
|
+
" and verified by read-back, but the change was NOT captured in update set " +
|
|
856
|
+
updateSetSysId +
|
|
857
|
+
" — it is live on this instance and cannot be promoted. Check the update set is " +
|
|
858
|
+
"in progress and in " +
|
|
859
|
+
ctx.table +
|
|
860
|
+
"'s scope.",
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
/** Which record type each change was written to — the two are not interchangeable, and
|
|
864
|
+
* a reader who does not know that will go looking for a label on the override row. */
|
|
865
|
+
function describeTargets(changes) {
|
|
866
|
+
var hasOverride = changes.some(function (c) {
|
|
867
|
+
return Boolean(overrideColumn_1.OVERRIDABLE[c.attribute]);
|
|
868
|
+
});
|
|
869
|
+
var hasLabel = changes.some(function (c) {
|
|
870
|
+
return c.attribute === "column_label";
|
|
871
|
+
});
|
|
872
|
+
if (hasOverride && hasLabel) {
|
|
873
|
+
return "sys_dictionary_override + sys_documentation";
|
|
874
|
+
}
|
|
875
|
+
return hasLabel ? "sys_documentation" : "sys_dictionary_override";
|
|
876
|
+
}
|
|
632
877
|
/** "max_length 40 -> 4000, mandatory false -> true" */
|
|
633
878
|
function describeChanges(changes) {
|
|
634
879
|
return changes
|