@tenonhq/dovetail-servicenow 0.0.48 → 0.0.49
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 +5 -1
- package/dist/cli.js +6 -2
- package/dist/cliUsage.js +13 -2
- package/dist/mcp/registry.js +8 -1
- package/dist/mcp/schemas.d.ts +3 -0
- package/dist/mcp/schemas.js +4 -0
- package/dist/table/addColumn.d.ts +34 -3
- package/dist/table/addColumn.js +217 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1151,7 +1151,11 @@ console.log(formatLayoutResult("form layout", result));
|
|
|
1151
1151
|
`dove-sn mcp` runs a self-contained MCP stdio server exposing the tools to
|
|
1152
1152
|
Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
|
|
1153
1153
|
`set_related_lists`, `add_choices_to_field`, the schema verbs `create_table` /
|
|
1154
|
-
`add_column`
|
|
1154
|
+
`add_column` (same-scope by default; `crossScope: true` with `scope` adds a column OWNED
|
|
1155
|
+
by another app — a Journey field on an Automate table, element `<scope>_<name>` —
|
|
1156
|
+
guarded by `sys_db_object.alter_access` and the update set's scope, with the stored
|
|
1157
|
+
element and `sys_scope` read back; the CLI flag is `--cross-scope`) /
|
|
1158
|
+
`add_index` (a single-column unique index via `sys_dictionary.unique`,
|
|
1155
1159
|
read back from the `v_db_index` view - uniqueness enforcement is always reported
|
|
1156
1160
|
unverified) / `index_list` (read-only: a table's database indexes from `v_db_index`,
|
|
1157
1161
|
the only index read surface - `sys_index` is API-level-ACL 403 and `sys_index_column`
|
package/dist/cli.js
CHANGED
|
@@ -1345,9 +1345,10 @@ async function runCreateTable(flags) {
|
|
|
1345
1345
|
* --table x_cadso_journey --label URL --type url
|
|
1346
1346
|
* [--name url] [--max-length 1024] [--reference <table>]
|
|
1347
1347
|
* [--mandatory] [--default <value>] [--dependent-on-field <element>]
|
|
1348
|
-
* [--scope x_cadso_journey] [--update-set <sys_id>]
|
|
1348
|
+
* [--scope x_cadso_journey] [--cross-scope] [--update-set <sys_id>]
|
|
1349
1349
|
* [--from-json <spec.json>] [--dry-run] [--debug] [--json]
|
|
1350
|
-
* --update-set is required unless --dry-run.
|
|
1350
|
+
* --update-set is required unless --dry-run. --cross-scope opts in to a column
|
|
1351
|
+
* OWNED by --scope when that differs from the table's scope.
|
|
1351
1352
|
*/
|
|
1352
1353
|
async function runAddColumn(flags) {
|
|
1353
1354
|
var spec = {};
|
|
@@ -1384,6 +1385,9 @@ async function runAddColumn(flags) {
|
|
|
1384
1385
|
var scope = flags.scope || spec.scope;
|
|
1385
1386
|
if (scope)
|
|
1386
1387
|
params.scope = scope;
|
|
1388
|
+
if (flags["cross-scope"] === "true" || spec.crossScope === true) {
|
|
1389
|
+
params.crossScope = true;
|
|
1390
|
+
}
|
|
1387
1391
|
var us = flags["update-set"] || spec.updateSetSysId;
|
|
1388
1392
|
if (us)
|
|
1389
1393
|
params.updateSetSysId = us;
|
package/dist/cliUsage.js
CHANGED
|
@@ -200,7 +200,15 @@ exports.VERB_USAGE = {
|
|
|
200
200
|
value: "<element>",
|
|
201
201
|
note: "Sibling column a document_id resolves against (its table_name column); must already exist.",
|
|
202
202
|
},
|
|
203
|
-
{
|
|
203
|
+
{
|
|
204
|
+
flag: "scope",
|
|
205
|
+
value: "<x_scope>",
|
|
206
|
+
note: "Owning app scope. Must match the table's scope unless --cross-scope is passed.",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
flag: "cross-scope",
|
|
210
|
+
note: "Opt in to a column OWNED by --scope, which differs from the table's scope (element becomes <scope>_<name>; the table must allow new fields; --update-set must be in --scope).",
|
|
211
|
+
},
|
|
204
212
|
{ flag: "update-set", value: "<sys_id>", note: "REQUIRED on the live path (only --dry-run works without one)." },
|
|
205
213
|
DRY_RUN_FLAG,
|
|
206
214
|
DEBUG_FLAG,
|
|
@@ -209,7 +217,10 @@ exports.VERB_USAGE = {
|
|
|
209
217
|
gate: "dry-run-flag",
|
|
210
218
|
gateNote: "--update-set is required unless --dry-run.",
|
|
211
219
|
example: "dove-sn add-column --table x_cadso_journey --label URL --type url --max-length 1024 --update-set <sys_id>",
|
|
212
|
-
notes: [
|
|
220
|
+
notes: [
|
|
221
|
+
"Exit 2 when the write landed but the read-back does not show the column.",
|
|
222
|
+
"Cross-scope: dove-sn add-column --table x_cadso_automate_email_batch --label 'Instance Step' --name instance_step --type reference --reference x_cadso_journey_instance_step --scope x_cadso_journey --cross-scope --update-set <journey set>",
|
|
223
|
+
],
|
|
213
224
|
},
|
|
214
225
|
"set-column": {
|
|
215
226
|
summary: "Update an EXISTING column's schema (label/mandatory/default/read-only/max-length/dependent-on-field), then verify",
|
package/dist/mcp/registry.js
CHANGED
|
@@ -440,7 +440,13 @@ function buildDescriptors(deps = {}) {
|
|
|
440
440
|
"table NAME; element is derived from label unless column.name is given. dependent_on_field names " +
|
|
441
441
|
"the sibling column a document_id column resolves against (its table_name column) — it must " +
|
|
442
442
|
"already exist on the table, is verified on the read-back, and is refused when absent. " +
|
|
443
|
-
"
|
|
443
|
+
"scope must match the table's scope unless crossScope:true, which opts in to a column OWNED " +
|
|
444
|
+
"by scope on another app's table (the platform's cross-scope field: element becomes " +
|
|
445
|
+
"<scope>_<name>, the dictionary row and its update-set capture land in scope); the table " +
|
|
446
|
+
"must allow new fields from other scopes (sys_db_object.alter_access) and updateSetSysId " +
|
|
447
|
+
"must belong to scope — both checked on dryRun too, and the stored element + sys_scope are " +
|
|
448
|
+
"read back. updateSetSysId is required on the live path. dryRun:true returns the plan with " +
|
|
449
|
+
"no writes.",
|
|
444
450
|
shape: schemas_1.addColumnSchema.shape,
|
|
445
451
|
handler: async function (args) {
|
|
446
452
|
var p = schemas_1.addColumnSchema.parse(args);
|
|
@@ -458,6 +464,7 @@ function buildDescriptors(deps = {}) {
|
|
|
458
464
|
table: p.table,
|
|
459
465
|
column: p.column,
|
|
460
466
|
scope: p.scope,
|
|
467
|
+
crossScope: p.crossScope,
|
|
461
468
|
updateSetSysId: p.updateSetSysId,
|
|
462
469
|
dryRun: p.dryRun,
|
|
463
470
|
debug: p.debug,
|
package/dist/mcp/schemas.d.ts
CHANGED
|
@@ -1890,6 +1890,7 @@ export declare var addColumnSchema: z.ZodObject<{
|
|
|
1890
1890
|
dependent_on_field?: string | undefined;
|
|
1891
1891
|
}>;
|
|
1892
1892
|
scope: z.ZodOptional<z.ZodString>;
|
|
1893
|
+
crossScope: z.ZodOptional<z.ZodBoolean>;
|
|
1893
1894
|
updateSetSysId: z.ZodOptional<z.ZodString>;
|
|
1894
1895
|
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
1895
1896
|
debug: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -1909,6 +1910,7 @@ export declare var addColumnSchema: z.ZodObject<{
|
|
|
1909
1910
|
debug?: boolean | undefined;
|
|
1910
1911
|
updateSetSysId?: string | undefined;
|
|
1911
1912
|
scope?: string | undefined;
|
|
1913
|
+
crossScope?: boolean | undefined;
|
|
1912
1914
|
}, {
|
|
1913
1915
|
table: string;
|
|
1914
1916
|
column: {
|
|
@@ -1925,6 +1927,7 @@ export declare var addColumnSchema: z.ZodObject<{
|
|
|
1925
1927
|
debug?: boolean | undefined;
|
|
1926
1928
|
updateSetSysId?: string | undefined;
|
|
1927
1929
|
scope?: string | undefined;
|
|
1930
|
+
crossScope?: boolean | undefined;
|
|
1928
1931
|
}>;
|
|
1929
1932
|
export declare var addIndexSchema: z.ZodObject<{
|
|
1930
1933
|
table: z.ZodString;
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -364,6 +364,10 @@ exports.addColumnSchema = zod_1.z.object({
|
|
|
364
364
|
table: zod_1.z.string().min(1),
|
|
365
365
|
column: exports.columnSpecSchema,
|
|
366
366
|
scope: zod_1.z.string().optional(),
|
|
367
|
+
// Explicit opt-in to a column OWNED by `scope` when that differs from the table's
|
|
368
|
+
// scope (a Journey field on an Automate table). Without it a mismatched scope is
|
|
369
|
+
// refused — on dryRun and live alike — because that is the classic wrong-scope slip.
|
|
370
|
+
crossScope: zod_1.z.boolean().optional(),
|
|
367
371
|
updateSetSysId: zod_1.z.string().optional(),
|
|
368
372
|
dryRun: zod_1.z.boolean().optional(),
|
|
369
373
|
debug: zod_1.z.boolean().optional(),
|
|
@@ -16,8 +16,23 @@
|
|
|
16
16
|
*
|
|
17
17
|
* The scope arg passed to `createRecord` is the scope NAME (e.g. "x_cadso_core"),
|
|
18
18
|
* not the sys_scope sys_id — the server-side changeScope matches on the scope name.
|
|
19
|
-
*
|
|
20
|
-
* table
|
|
19
|
+
* By default a column belongs to its table's scope, so the name is resolved from the
|
|
20
|
+
* table and an explicit `scope` override must match it.
|
|
21
|
+
*
|
|
22
|
+
* CROSS-SCOPE COLUMNS (`crossScope: true`). ServiceNow lets one app add a column to
|
|
23
|
+
* another app's table — Studio with app = Journey adding a field to an Automate table
|
|
24
|
+
* — and prefixes the element with the OWNING scope: `x_cadso_journey_instance_step`
|
|
25
|
+
* on `x_cadso_automate_email_batch`, dictionary row owned by x_cadso_journey. This is
|
|
26
|
+
* the column-ownership rule for Tenon's layered apps, so it is supported here as an
|
|
27
|
+
* explicit opt-in: `scope` names the column's scope, `crossScope: true` acknowledges it
|
|
28
|
+
* differs from the table's. The insert runs through the same scope-aware op switched
|
|
29
|
+
* to the COLUMN's scope, so the dictionary row and its update-set capture both land
|
|
30
|
+
* in the column's scope. Guards, run on dry-run and live alike: the override scope
|
|
31
|
+
* must exist; the table must allow new fields from other scopes
|
|
32
|
+
* (`sys_db_object.alter_access`); the update set must belong to the column's scope.
|
|
33
|
+
* The element is sent already prefixed (`<scope>_<name>`; an already-prefixed name
|
|
34
|
+
* is accepted as-is) and the element ServiceNow actually stored is read back and
|
|
35
|
+
* reported, together with a `sys_scope` assertion on the read-back row.
|
|
21
36
|
*
|
|
22
37
|
* SIZING THE PHYSICAL COLUMN. A max_length carried on the INSERT sets the dictionary
|
|
23
38
|
* row but NOT the column ServiceNow actually builds — it materialises at the platform
|
|
@@ -46,8 +61,19 @@ export interface AddColumnParams {
|
|
|
46
61
|
table: string;
|
|
47
62
|
/** The single column to add. `name` (the element) is optional; derived from label when omitted. */
|
|
48
63
|
column: ColumnSpec;
|
|
49
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Scope name or sys_scope sys_id. Must match the table's own scope (a column lives
|
|
66
|
+
* there) — unless `crossScope` is true, in which case it names the scope that will OWN
|
|
67
|
+
* the column (e.g. "x_cadso_journey" for a Journey column on an Automate table).
|
|
68
|
+
*/
|
|
50
69
|
scope?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Explicit opt-in to a column owned by a different scope than its table. Requires
|
|
72
|
+
* `scope`. The element is prefixed with the owning scope (`x_cadso_journey_<name>`),
|
|
73
|
+
* the table must allow new fields from other scopes (sys_db_object.alter_access),
|
|
74
|
+
* and the update set must belong to the column's scope.
|
|
75
|
+
*/
|
|
76
|
+
crossScope?: boolean;
|
|
51
77
|
/** Update set sys_id to capture the insert into. REQUIRED on the live path (dry-run doesn't need it). */
|
|
52
78
|
updateSetSysId?: string;
|
|
53
79
|
/** Emit diagnostic detail in the result note. */
|
|
@@ -67,6 +93,11 @@ export interface AddColumnResult {
|
|
|
67
93
|
label: string;
|
|
68
94
|
/** Resolved ServiceNow internal_type (friendly -> internal). */
|
|
69
95
|
internalType: string;
|
|
96
|
+
/**
|
|
97
|
+
* Scope NAME that owns the column ("" on a network-free dry-run). Equals the table's
|
|
98
|
+
* scope unless `crossScope` was used, in which case it is the override scope.
|
|
99
|
+
*/
|
|
100
|
+
scope: string;
|
|
70
101
|
/** sys_id of the sys_dictionary row (the insert's, or the existing row's on "skipped"). */
|
|
71
102
|
columnSysId: string;
|
|
72
103
|
/** Update set the write was captured into ("" on dry-run). */
|
package/dist/table/addColumn.js
CHANGED
|
@@ -17,8 +17,23 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The scope arg passed to `createRecord` is the scope NAME (e.g. "x_cadso_core"),
|
|
19
19
|
* not the sys_scope sys_id — the server-side changeScope matches on the scope name.
|
|
20
|
-
*
|
|
21
|
-
* table
|
|
20
|
+
* By default a column belongs to its table's scope, so the name is resolved from the
|
|
21
|
+
* table and an explicit `scope` override must match it.
|
|
22
|
+
*
|
|
23
|
+
* CROSS-SCOPE COLUMNS (`crossScope: true`). ServiceNow lets one app add a column to
|
|
24
|
+
* another app's table — Studio with app = Journey adding a field to an Automate table
|
|
25
|
+
* — and prefixes the element with the OWNING scope: `x_cadso_journey_instance_step`
|
|
26
|
+
* on `x_cadso_automate_email_batch`, dictionary row owned by x_cadso_journey. This is
|
|
27
|
+
* the column-ownership rule for Tenon's layered apps, so it is supported here as an
|
|
28
|
+
* explicit opt-in: `scope` names the column's scope, `crossScope: true` acknowledges it
|
|
29
|
+
* differs from the table's. The insert runs through the same scope-aware op switched
|
|
30
|
+
* to the COLUMN's scope, so the dictionary row and its update-set capture both land
|
|
31
|
+
* in the column's scope. Guards, run on dry-run and live alike: the override scope
|
|
32
|
+
* must exist; the table must allow new fields from other scopes
|
|
33
|
+
* (`sys_db_object.alter_access`); the update set must belong to the column's scope.
|
|
34
|
+
* The element is sent already prefixed (`<scope>_<name>`; an already-prefixed name
|
|
35
|
+
* is accepted as-is) and the element ServiceNow actually stored is read back and
|
|
36
|
+
* reported, together with a `sys_scope` assertion on the read-back row.
|
|
22
37
|
*
|
|
23
38
|
* SIZING THE PHYSICAL COLUMN. A max_length carried on the INSERT sets the dictionary
|
|
24
39
|
* row but NOT the column ServiceNow actually builds — it materialises at the platform
|
|
@@ -60,6 +75,7 @@ var READ_BACK_FIELDS = [
|
|
|
60
75
|
"internal_type",
|
|
61
76
|
"max_length",
|
|
62
77
|
"dependent_on_field",
|
|
78
|
+
"sys_scope",
|
|
63
79
|
];
|
|
64
80
|
/** Patch max_length on a sys_dictionary row, captured in the given update set. */
|
|
65
81
|
async function setMaxLength(client, columnSysId, updateSetSysId, length) {
|
|
@@ -121,10 +137,10 @@ function validate(params) {
|
|
|
121
137
|
if (!params.column || typeof params.column !== "object")
|
|
122
138
|
throw new Error("add-column: column is required.");
|
|
123
139
|
}
|
|
124
|
-
/** Resolve the table by name or sys_id; returns its name, sys_id, and
|
|
140
|
+
/** Resolve the table by name or sys_id; returns its name, sys_id, scope, and alter_access. */
|
|
125
141
|
async function resolveTable(client, table) {
|
|
126
142
|
var query = SYS_ID.test(table) ? "sys_id=" + table : "name=" + table;
|
|
127
|
-
var rows = await client.table.query("sys_db_object", query, { limit: 1, fields: ["sys_id", "name", "sys_scope"] });
|
|
143
|
+
var rows = await client.table.query("sys_db_object", query, { limit: 1, fields: ["sys_id", "name", "sys_scope", "alter_access"] });
|
|
128
144
|
if (rows.length === 0) {
|
|
129
145
|
throw new Error("add-column: table '" + table + "' not found in sys_db_object.");
|
|
130
146
|
}
|
|
@@ -132,6 +148,126 @@ async function resolveTable(client, table) {
|
|
|
132
148
|
name: (0, setField_1.fieldToString)(rows[0].name) || table,
|
|
133
149
|
sysId: (0, setField_1.fieldToString)(rows[0].sys_id),
|
|
134
150
|
scopeSysId: (0, setField_1.fieldToString)(rows[0].sys_scope),
|
|
151
|
+
alterAccess: (0, setField_1.fieldToString)(rows[0].alter_access),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Resolve a scope by NAME or sys_id to both; empty strings when not found. */
|
|
155
|
+
async function resolveScope(client, scope) {
|
|
156
|
+
var query = SYS_ID.test(scope) ? "sys_id=" + scope : "scope=" + scope;
|
|
157
|
+
var rows = await client.table.query("sys_scope", query, { limit: 1, fields: ["sys_id", "scope"] });
|
|
158
|
+
if (rows.length === 0)
|
|
159
|
+
return { name: "", sysId: "" };
|
|
160
|
+
return {
|
|
161
|
+
name: (0, setField_1.fieldToString)(rows[0].scope),
|
|
162
|
+
sysId: (0, setField_1.fieldToString)(rows[0].sys_id),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Resolve an update set's owning application (sys_scope sys_id); "" when not found. */
|
|
166
|
+
async function resolveUpdateSetScope(client, updateSetSysId) {
|
|
167
|
+
var rows = await client.table.query("sys_update_set", "sys_id=" + updateSetSysId, { limit: 1, fields: ["sys_id", "name", "application"] });
|
|
168
|
+
if (rows.length === 0)
|
|
169
|
+
return { found: false, applicationSysId: "", name: "" };
|
|
170
|
+
return {
|
|
171
|
+
found: true,
|
|
172
|
+
applicationSysId: (0, setField_1.fieldToString)(rows[0].application),
|
|
173
|
+
name: (0, setField_1.fieldToString)(rows[0].name),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Resolve the table + decide which scope owns the column, running every scope guard.
|
|
178
|
+
* Shared by the dry-run and live paths so a request that would fail live fails the
|
|
179
|
+
* dry-run the same way — a dry-run that skips the guards is worse than none.
|
|
180
|
+
*/
|
|
181
|
+
async function planColumnScope(client, params, element) {
|
|
182
|
+
var resolved = await resolveTable(client, params.table);
|
|
183
|
+
var tableScopeName = await resolveScopeName(client, resolved.scopeSysId);
|
|
184
|
+
if (!tableScopeName) {
|
|
185
|
+
throw new Error("add-column: could not resolve the scope name for table '" +
|
|
186
|
+
resolved.name +
|
|
187
|
+
"' (sys_scope " +
|
|
188
|
+
(resolved.scopeSysId || "(none)") +
|
|
189
|
+
") — needed to scope the insert correctly.");
|
|
190
|
+
}
|
|
191
|
+
var override = params.scope ? params.scope.trim() : "";
|
|
192
|
+
var wantsCross = params.crossScope === true;
|
|
193
|
+
if (wantsCross && !override) {
|
|
194
|
+
throw new Error("add-column: crossScope requires --scope naming the scope that will OWN the column " +
|
|
195
|
+
"(e.g. x_cadso_journey for a Journey column on an Automate table).");
|
|
196
|
+
}
|
|
197
|
+
var sameScope = !override ||
|
|
198
|
+
override === tableScopeName ||
|
|
199
|
+
override === resolved.scopeSysId;
|
|
200
|
+
if (sameScope) {
|
|
201
|
+
return {
|
|
202
|
+
resolved: resolved,
|
|
203
|
+
scopeName: tableScopeName,
|
|
204
|
+
scopeSysId: resolved.scopeSysId,
|
|
205
|
+
crossScope: false,
|
|
206
|
+
element: element,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// The override names a scope other than the table's. Without the explicit opt-in
|
|
210
|
+
// this is the classic wrong-scope mistake, so refuse — on dry-run and live alike.
|
|
211
|
+
if (!wantsCross) {
|
|
212
|
+
throw new Error("add-column: --scope '" +
|
|
213
|
+
override +
|
|
214
|
+
"' does not match table '" +
|
|
215
|
+
resolved.name +
|
|
216
|
+
"' scope '" +
|
|
217
|
+
tableScopeName +
|
|
218
|
+
"' — a column lives in its table's scope. Omit --scope or set it to '" +
|
|
219
|
+
tableScopeName +
|
|
220
|
+
"'. To add a column OWNED by '" +
|
|
221
|
+
override +
|
|
222
|
+
"' (a cross-scope field, element prefixed '" +
|
|
223
|
+
override +
|
|
224
|
+
"_'), pass --cross-scope.");
|
|
225
|
+
}
|
|
226
|
+
var owner = await resolveScope(client, override);
|
|
227
|
+
if (!owner.name || !owner.sysId) {
|
|
228
|
+
throw new Error("add-column: cross-scope owner '" +
|
|
229
|
+
override +
|
|
230
|
+
"' was not found in sys_scope — pass the scope name (x_cadso_journey) or its sys_id.");
|
|
231
|
+
}
|
|
232
|
+
if (resolved.alterAccess !== "true") {
|
|
233
|
+
throw new Error("add-column: table '" +
|
|
234
|
+
resolved.name +
|
|
235
|
+
"' does not allow new fields from other scopes (sys_db_object.alter_access is '" +
|
|
236
|
+
(resolved.alterAccess || "(empty)") +
|
|
237
|
+
"'). Enable 'Allow new fields' on the table in its own scope, or add the column " +
|
|
238
|
+
"in scope '" +
|
|
239
|
+
tableScopeName +
|
|
240
|
+
"' instead.");
|
|
241
|
+
}
|
|
242
|
+
if (params.updateSetSysId && params.updateSetSysId.trim()) {
|
|
243
|
+
var us = await resolveUpdateSetScope(client, params.updateSetSysId.trim());
|
|
244
|
+
if (!us.found) {
|
|
245
|
+
throw new Error("add-column: update set '" +
|
|
246
|
+
params.updateSetSysId +
|
|
247
|
+
"' was not found in sys_update_set.");
|
|
248
|
+
}
|
|
249
|
+
if (us.applicationSysId !== owner.sysId) {
|
|
250
|
+
throw new Error("add-column: update set '" +
|
|
251
|
+
(us.name || params.updateSetSysId) +
|
|
252
|
+
"' does not belong to the column's scope '" +
|
|
253
|
+
owner.name +
|
|
254
|
+
"' — a cross-scope column is captured in an update set of the scope that OWNS " +
|
|
255
|
+
"it, not the table's. Pass an update set in '" +
|
|
256
|
+
owner.name +
|
|
257
|
+
"'.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// ServiceNow stores a cross-scope element as <owner>_<name>. Send it already
|
|
261
|
+
// prefixed so the pre-check, the insert, and the read-back all agree on the one
|
|
262
|
+
// element; an already-prefixed name is accepted as-is.
|
|
263
|
+
var prefix = owner.name + "_";
|
|
264
|
+
var prefixed = element.indexOf(prefix) === 0 ? element : prefix + element;
|
|
265
|
+
return {
|
|
266
|
+
resolved: resolved,
|
|
267
|
+
scopeName: owner.name,
|
|
268
|
+
scopeSysId: owner.sysId,
|
|
269
|
+
crossScope: true,
|
|
270
|
+
element: prefixed,
|
|
135
271
|
};
|
|
136
272
|
}
|
|
137
273
|
/** Resolve a sys_scope sys_id to its scope NAME (e.g. "x_cadso_core"). */
|
|
@@ -170,23 +306,47 @@ async function addColumn(params) {
|
|
|
170
306
|
"' names the column being added — a column cannot depend on itself.");
|
|
171
307
|
}
|
|
172
308
|
if (params.dryRun) {
|
|
309
|
+
// A plain dry-run (no scope named) is pure + deterministic — no network — so it
|
|
310
|
+
// can plan without an instance. Once a scope IS named, the plan depends on the
|
|
311
|
+
// instance (does the table allow it? does the update set match?), so the dry-run
|
|
312
|
+
// runs the SAME guards as the live path and fails the same way. It used to skip
|
|
313
|
+
// them, so a cross-scope request dry-ran clean and only failed live.
|
|
314
|
+
var hasScopeAsk = (params.scope !== undefined && params.scope.trim() !== "") ||
|
|
315
|
+
params.crossScope === true;
|
|
316
|
+
var planned;
|
|
317
|
+
if (hasScopeAsk)
|
|
318
|
+
planned = await planColumnScope(client, params, element);
|
|
319
|
+
var planElement = planned ? planned.element : element;
|
|
320
|
+
var planTable = planned ? planned.resolved.name : params.table;
|
|
321
|
+
var planTableSysId = planned
|
|
322
|
+
? planned.resolved.sysId
|
|
323
|
+
: SYS_ID.test(params.table)
|
|
324
|
+
? params.table
|
|
325
|
+
: "";
|
|
173
326
|
return {
|
|
174
327
|
status: "dry-run",
|
|
175
|
-
table:
|
|
176
|
-
tableSysId:
|
|
177
|
-
element:
|
|
328
|
+
table: planTable,
|
|
329
|
+
tableSysId: planTableSysId,
|
|
330
|
+
element: planElement,
|
|
178
331
|
label: col.label,
|
|
179
332
|
internalType: col.type,
|
|
333
|
+
scope: planned ? planned.scopeName : "",
|
|
180
334
|
columnSysId: "",
|
|
181
335
|
updateSetSysId: params.updateSetSysId ? params.updateSetSysId : "",
|
|
182
336
|
verified: false,
|
|
183
337
|
note: "dry-run: no write. Would add column '" +
|
|
184
|
-
|
|
338
|
+
planElement +
|
|
185
339
|
"' (" +
|
|
186
340
|
col.type +
|
|
187
341
|
") to '" +
|
|
188
|
-
|
|
189
|
-
"'
|
|
342
|
+
planTable +
|
|
343
|
+
"'" +
|
|
344
|
+
(planned && planned.crossScope
|
|
345
|
+
? " OWNED BY scope '" +
|
|
346
|
+
planned.scopeName +
|
|
347
|
+
"' (cross-scope: the table allows new fields and the update set is in that scope)"
|
|
348
|
+
: "") +
|
|
349
|
+
" via a scope-aware sys_dictionary insert, captured into update set " +
|
|
190
350
|
(params.updateSetSysId ? params.updateSetSysId : "(none provided)") +
|
|
191
351
|
(wantDependent
|
|
192
352
|
? ", dependent on column '" +
|
|
@@ -203,31 +363,13 @@ async function addColumn(params) {
|
|
|
203
363
|
throw new Error("add-column: updateSetSysId is required on the live path so the sys_dictionary " +
|
|
204
364
|
"insert is captured in a known update set (dry-run does not need one).");
|
|
205
365
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
") — needed to scope the insert correctly.");
|
|
214
|
-
}
|
|
215
|
-
// A column lives in its table's scope. An explicit override must match it (by name
|
|
216
|
-
// or sys_id); we never write a column into a scope other than its table's.
|
|
217
|
-
if (params.scope && params.scope.trim()) {
|
|
218
|
-
var override = params.scope.trim();
|
|
219
|
-
if (override !== scopeName && override !== resolved.scopeSysId) {
|
|
220
|
-
throw new Error("add-column: --scope '" +
|
|
221
|
-
override +
|
|
222
|
-
"' does not match table '" +
|
|
223
|
-
resolved.name +
|
|
224
|
-
"' scope '" +
|
|
225
|
-
scopeName +
|
|
226
|
-
"' — a column must live in its table's scope. Omit --scope or set it to '" +
|
|
227
|
-
scopeName +
|
|
228
|
-
"'.");
|
|
229
|
-
}
|
|
230
|
-
}
|
|
366
|
+
// Resolve the table and decide which scope OWNS the column — the table's by default,
|
|
367
|
+
// the override's under crossScope. Every scope guard runs inside planColumnScope, the
|
|
368
|
+
// same code the dry-run ran, so nothing can pass dry-run and fail here on scope.
|
|
369
|
+
var plan = await planColumnScope(client, params, element);
|
|
370
|
+
var resolved = plan.resolved;
|
|
371
|
+
var scopeName = plan.scopeName;
|
|
372
|
+
element = plan.element;
|
|
231
373
|
// max_length is deliberately NOT sent on the insert — see the sizing step below.
|
|
232
374
|
// Reference (and date) columns carry no max_length at all.
|
|
233
375
|
var wantLength = col.type === "reference" ? "" : col.maxLength;
|
|
@@ -289,6 +431,7 @@ async function addColumn(params) {
|
|
|
289
431
|
element: existingElement,
|
|
290
432
|
label: col.label,
|
|
291
433
|
internalType: existingType || col.type,
|
|
434
|
+
scope: scopeName,
|
|
292
435
|
columnSysId: existingSysId,
|
|
293
436
|
updateSetSysId: params.updateSetSysId,
|
|
294
437
|
verified: drift.length === 0,
|
|
@@ -320,7 +463,10 @@ async function addColumn(params) {
|
|
|
320
463
|
mandatory: params.column.mandatory === true ? "true" : "false",
|
|
321
464
|
default_value: typeof params.column.default === "string" ? params.column.default : "",
|
|
322
465
|
active: "true",
|
|
323
|
-
|
|
466
|
+
// The COLUMN's owner — the table's scope by default, the override's under
|
|
467
|
+
// crossScope. The createRecord `scope` below is switched to the same owner so the
|
|
468
|
+
// dictionary row and its update-set capture agree on who owns the column.
|
|
469
|
+
sys_scope: plan.scopeSysId,
|
|
324
470
|
};
|
|
325
471
|
// Reference columns carry the target table NAME (not a sys_id) in `reference`.
|
|
326
472
|
if (col.reference)
|
|
@@ -341,11 +487,11 @@ async function addColumn(params) {
|
|
|
341
487
|
}
|
|
342
488
|
catch (e) {
|
|
343
489
|
return failure(resolved, col, element, params.updateSetSysId, "sys_dictionary insert failed: " +
|
|
344
|
-
(e && e.message ? e.message : String(e)));
|
|
490
|
+
(e && e.message ? e.message : String(e)), undefined, scopeName);
|
|
345
491
|
}
|
|
346
492
|
var columnSysId = (0, setField_1.fieldToString)(created && created.sys_id);
|
|
347
493
|
if (!columnSysId) {
|
|
348
|
-
return failure(resolved, col, element, params.updateSetSysId, "createRecord returned no sys_id — the insert may not have landed; check the instance.");
|
|
494
|
+
return failure(resolved, col, element, params.updateSetSysId, "createRecord returned no sys_id — the insert may not have landed; check the instance.", undefined, scopeName);
|
|
349
495
|
}
|
|
350
496
|
// SIZE THE PHYSICAL COLUMN. A max_length carried on the INSERT sets the dictionary
|
|
351
497
|
// row but NOT the column ServiceNow actually builds — it materialises at the platform
|
|
@@ -389,7 +535,7 @@ async function addColumn(params) {
|
|
|
389
535
|
" was created, but sizing/verifying it failed: " +
|
|
390
536
|
(e && e.message ? e.message : String(e)) +
|
|
391
537
|
" — the column EXISTS but may not be the size it was declared, so treat it as " +
|
|
392
|
-
"unsafe to write to until it is checked on the instance.", columnSysId);
|
|
538
|
+
"unsafe to write to until it is checked on the instance.", columnSysId, scopeName);
|
|
393
539
|
}
|
|
394
540
|
// The read-back proves THIS insert landed (by sys_id, not element — ServiceNow can
|
|
395
541
|
// normalise the element server-side) AND that the column is the size it was asked to
|
|
@@ -402,6 +548,23 @@ async function addColumn(params) {
|
|
|
402
548
|
var readBackDependent = verified
|
|
403
549
|
? (0, setField_1.fieldToString)(rows[0].dependent_on_field)
|
|
404
550
|
: "";
|
|
551
|
+
var readBackScope = verified ? (0, setField_1.fieldToString)(rows[0].sys_scope) : "";
|
|
552
|
+
// Ownership is part of the column's identity: a cross-scope row that reads back in
|
|
553
|
+
// the TABLE's scope was prefixed for nothing and will ship in the wrong update set.
|
|
554
|
+
// Asserted whenever the instance reports a scope (a read-back that omits it, as in
|
|
555
|
+
// older stubs, is not evidence either way).
|
|
556
|
+
if (verified && readBackScope && readBackScope !== plan.scopeSysId) {
|
|
557
|
+
return failure(resolved, col, element, params.updateSetSysId, "column '" +
|
|
558
|
+
actualElement +
|
|
559
|
+
"' materialised but sys_scope read back as '" +
|
|
560
|
+
readBackScope +
|
|
561
|
+
"', not the requested '" +
|
|
562
|
+
plan.scopeSysId +
|
|
563
|
+
"' (" +
|
|
564
|
+
scopeName +
|
|
565
|
+
") — the column is owned by the wrong app and its capture will not promote with " +
|
|
566
|
+
"the right scope. Reconcile it on the instance before writing to it.", columnSysId, scopeName);
|
|
567
|
+
}
|
|
405
568
|
if (verified && wantLength && readBackLength !== wantLength) {
|
|
406
569
|
return failure(resolved, col, element, params.updateSetSysId, "column '" +
|
|
407
570
|
actualElement +
|
|
@@ -411,7 +574,7 @@ async function addColumn(params) {
|
|
|
411
574
|
wantLength +
|
|
412
575
|
"' — the physical column is NOT the size it was declared, so values over the " +
|
|
413
576
|
"real limit would be silently truncated. Fix the column on the instance before " +
|
|
414
|
-
"writing to it.", columnSysId);
|
|
577
|
+
"writing to it.", columnSysId, scopeName);
|
|
415
578
|
}
|
|
416
579
|
if (verified && wantDependent && readBackDependent !== wantDependent) {
|
|
417
580
|
return failure(resolved, col, element, params.updateSetSysId, "column '" +
|
|
@@ -421,13 +584,13 @@ async function addColumn(params) {
|
|
|
421
584
|
", not the requested '" +
|
|
422
585
|
wantDependent +
|
|
423
586
|
"' — a document_id with no dependency resolves against nothing. Set it on " +
|
|
424
|
-
"the instance (set-column --dependent-on-field) before writing to the column.", columnSysId);
|
|
587
|
+
"the instance (set-column --dependent-on-field) before writing to the column.", columnSysId, scopeName);
|
|
425
588
|
}
|
|
426
589
|
if (!verified) {
|
|
427
590
|
return failure(resolved, col, element, params.updateSetSysId, "createRecord returned sys_id " +
|
|
428
591
|
columnSysId +
|
|
429
592
|
" but no sys_dictionary row was found on read-back — the column may not have " +
|
|
430
|
-
"materialised; check the instance.", columnSysId);
|
|
593
|
+
"materialised; check the instance.", columnSysId, scopeName);
|
|
431
594
|
}
|
|
432
595
|
// Same trust-the-read-back rule as max_length: a column that reads back a different
|
|
433
596
|
// internal_type than was requested is NOT the column that was asked for. Refuse to
|
|
@@ -441,7 +604,7 @@ async function addColumn(params) {
|
|
|
441
604
|
"', not the requested '" +
|
|
442
605
|
col.type +
|
|
443
606
|
"' — the column that exists is not the column that was asked for. Reconcile it " +
|
|
444
|
-
"on the instance before writing to it.", columnSysId);
|
|
607
|
+
"on the instance before writing to it.", columnSysId, scopeName);
|
|
445
608
|
}
|
|
446
609
|
var note = "Added column '" +
|
|
447
610
|
actualElement +
|
|
@@ -450,6 +613,9 @@ async function addColumn(params) {
|
|
|
450
613
|
") to " +
|
|
451
614
|
resolved.name +
|
|
452
615
|
(wantDependent ? ", dependent on '" + wantDependent + "'" : "") +
|
|
616
|
+
(plan.crossScope
|
|
617
|
+
? " owned by scope '" + scopeName + "' (cross-scope field)"
|
|
618
|
+
: "") +
|
|
453
619
|
" — verified present in sys_dictionary, captured into update set " +
|
|
454
620
|
params.updateSetSysId +
|
|
455
621
|
(actualElement !== element
|
|
@@ -467,7 +633,11 @@ async function addColumn(params) {
|
|
|
467
633
|
" scopeName=" +
|
|
468
634
|
scopeName +
|
|
469
635
|
" sys_scope=" +
|
|
470
|
-
|
|
636
|
+
plan.scopeSysId +
|
|
637
|
+
" crossScope=" +
|
|
638
|
+
String(plan.crossScope) +
|
|
639
|
+
" readBackScope=" +
|
|
640
|
+
(readBackScope || "(none)") +
|
|
471
641
|
" readBackType=" +
|
|
472
642
|
(readBackType || "(none)") +
|
|
473
643
|
"]";
|
|
@@ -479,6 +649,7 @@ async function addColumn(params) {
|
|
|
479
649
|
element: actualElement,
|
|
480
650
|
label: col.label,
|
|
481
651
|
internalType: col.type,
|
|
652
|
+
scope: scopeName,
|
|
482
653
|
columnSysId: columnSysId,
|
|
483
654
|
updateSetSysId: params.updateSetSysId,
|
|
484
655
|
verified: true,
|
|
@@ -486,7 +657,7 @@ async function addColumn(params) {
|
|
|
486
657
|
};
|
|
487
658
|
}
|
|
488
659
|
/** Build a `failed` result (shared by the insert-threw and read-back-empty paths). */
|
|
489
|
-
function failure(resolved, col, element, updateSetSysId, note, columnSysId) {
|
|
660
|
+
function failure(resolved, col, element, updateSetSysId, note, columnSysId, scope) {
|
|
490
661
|
return {
|
|
491
662
|
status: "failed",
|
|
492
663
|
table: resolved.name,
|
|
@@ -494,6 +665,7 @@ function failure(resolved, col, element, updateSetSysId, note, columnSysId) {
|
|
|
494
665
|
element: element,
|
|
495
666
|
label: col.label,
|
|
496
667
|
internalType: col.type,
|
|
668
|
+
scope: scope ? scope : "",
|
|
497
669
|
columnSysId: columnSysId ? columnSysId : "",
|
|
498
670
|
updateSetSysId: updateSetSysId,
|
|
499
671
|
verified: false,
|