@tenonhq/dovetail-servicenow 0.0.19 → 0.0.20
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 +15 -8
- package/dist/cli.js +5 -0
- package/dist/mcp/schemas.d.ts +6 -0
- package/dist/mcp/schemas.js +3 -1
- package/dist/table/buildTableSave.d.ts +9 -0
- package/dist/table/buildTableSave.js +10 -0
- package/dist/table/createTable.d.ts +33 -7
- package/dist/table/createTable.js +81 -19
- package/dist/table/formSession.d.ts +20 -7
- package/dist/table/formSession.js +44 -7
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -237,14 +237,21 @@ insert into `sys_db_object` **orphans** the table (a metadata row with no physic
|
|
|
237
237
|
table, no ACLs, no scope wiring). So this replays the Studio form save: a single
|
|
238
238
|
`POST /sys_db_object.do` (form-login session → `g_ck`) whose body embeds every
|
|
239
239
|
column as a `sys_dictionary` list-edit XML blob (one `<record operation="add">`
|
|
240
|
-
per column) plus the Application-Navigator module — yielding the real
|
|
241
|
-
graph + the physical table + seeded ACLs.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
per column) plus the Application-Navigator module — yielding the real platform
|
|
241
|
+
graph + the physical table + seeded ACLs. Before the save it switches the form
|
|
242
|
+
session's **current application** to the target scope (the new table's scope is
|
|
243
|
+
governed by the session app, not a form field), so the table, columns, ACLs, and
|
|
244
|
+
module all land in `--scope`. Friendly column types are mapped to ServiceNow
|
|
245
|
+
internal types (`string` → `string_full_utf8`, Studio's real String type).
|
|
246
|
+
|
|
247
|
+
`--dry-run` returns the plan + the column XML + the projected graph with no session
|
|
248
|
+
and no writes. **Validated live 2026-06-13** (tenonworkstudio): a 6-column create
|
|
249
|
+
landed the correctly-scoped table, all columns, ACLs + role, the module, and the
|
|
250
|
+
full graph in the pinned update set, and a scoped insert round-tripped. Still: pin
|
|
251
|
+
`--update-set <sys_id>` and verify the `sys_update_xml` rows afterward. `--debug`
|
|
252
|
+
adds diagnostics (app-switch status, resolved column key, assigned sys_id) to the
|
|
253
|
+
result note. Ground truth (the HAR dissection) lives in the CTO repo's create-table
|
|
254
|
+
docs.
|
|
248
255
|
|
|
249
256
|
`test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
|
|
250
257
|
declared variables?) that never runs the flow; `--execute --confirm` runs it via
|
package/dist/cli.js
CHANGED
|
@@ -785,8 +785,13 @@ async function runCreateTable(flags) {
|
|
|
785
785
|
var sa = flags["save-action"] || spec.saveActionSysId;
|
|
786
786
|
if (sa)
|
|
787
787
|
params.saveActionSysId = sa;
|
|
788
|
+
var relId = flags["columns-rel-id"] || spec.columnsRelId;
|
|
789
|
+
if (relId)
|
|
790
|
+
params.columnsRelId = relId;
|
|
788
791
|
if (flags["dry-run"] === "true" || spec.dryRun === true)
|
|
789
792
|
params.dryRun = true;
|
|
793
|
+
if (flags.debug === "true" || spec.debug === true)
|
|
794
|
+
params.debug = true;
|
|
790
795
|
var result = await (0, table_1.createTable)(params);
|
|
791
796
|
if (flags.json === "true") {
|
|
792
797
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
package/dist/mcp/schemas.d.ts
CHANGED
|
@@ -432,7 +432,9 @@ export declare var createTableSchema: z.ZodObject<{
|
|
|
432
432
|
showInMenu: z.ZodOptional<z.ZodBoolean>;
|
|
433
433
|
updateSetSysId: z.ZodOptional<z.ZodString>;
|
|
434
434
|
saveActionSysId: z.ZodOptional<z.ZodString>;
|
|
435
|
+
columnsRelId: z.ZodOptional<z.ZodString>;
|
|
435
436
|
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
437
|
+
debug: z.ZodOptional<z.ZodBoolean>;
|
|
436
438
|
}, "strip", z.ZodTypeAny, {
|
|
437
439
|
name: string;
|
|
438
440
|
columns: {
|
|
@@ -445,6 +447,7 @@ export declare var createTableSchema: z.ZodObject<{
|
|
|
445
447
|
label: string;
|
|
446
448
|
scope: string;
|
|
447
449
|
dryRun?: boolean | undefined;
|
|
450
|
+
debug?: boolean | undefined;
|
|
448
451
|
updateSetSysId?: string | undefined;
|
|
449
452
|
extendsTable?: string | undefined;
|
|
450
453
|
numberPrefix?: string | undefined;
|
|
@@ -453,6 +456,7 @@ export declare var createTableSchema: z.ZodObject<{
|
|
|
453
456
|
access?: string | undefined;
|
|
454
457
|
showInMenu?: boolean | undefined;
|
|
455
458
|
saveActionSysId?: string | undefined;
|
|
459
|
+
columnsRelId?: string | undefined;
|
|
456
460
|
}, {
|
|
457
461
|
name: string;
|
|
458
462
|
columns: {
|
|
@@ -465,6 +469,7 @@ export declare var createTableSchema: z.ZodObject<{
|
|
|
465
469
|
label: string;
|
|
466
470
|
scope: string;
|
|
467
471
|
dryRun?: boolean | undefined;
|
|
472
|
+
debug?: boolean | undefined;
|
|
468
473
|
updateSetSysId?: string | undefined;
|
|
469
474
|
extendsTable?: string | undefined;
|
|
470
475
|
numberPrefix?: string | undefined;
|
|
@@ -473,4 +478,5 @@ export declare var createTableSchema: z.ZodObject<{
|
|
|
473
478
|
access?: string | undefined;
|
|
474
479
|
showInMenu?: boolean | undefined;
|
|
475
480
|
saveActionSysId?: string | undefined;
|
|
481
|
+
columnsRelId?: string | undefined;
|
|
476
482
|
}>;
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -133,5 +133,7 @@ exports.createTableSchema = zod_1.z.object({
|
|
|
133
133
|
showInMenu: zod_1.z.boolean().optional(),
|
|
134
134
|
updateSetSysId: zod_1.z.string().optional(),
|
|
135
135
|
saveActionSysId: zod_1.z.string().optional(),
|
|
136
|
-
|
|
136
|
+
columnsRelId: zod_1.z.string().optional(),
|
|
137
|
+
dryRun: zod_1.z.boolean().optional(),
|
|
138
|
+
debug: zod_1.z.boolean().optional()
|
|
137
139
|
});
|
|
@@ -48,6 +48,15 @@ export interface OverlaySpec {
|
|
|
48
48
|
superClassLabel: string;
|
|
49
49
|
scopeSysId: string;
|
|
50
50
|
scopeLabel: string;
|
|
51
|
+
/**
|
|
52
|
+
* Scope sys_id to stamp the transaction with. A new table's scope is governed
|
|
53
|
+
* by the form transaction's scope, NOT the `sys_db_object.sys_scope` field — and
|
|
54
|
+
* a form-login session can't reliably switch its current application. Setting
|
|
55
|
+
* `sysparm_transaction_scope` / `sysparm_record_scope` explicitly is the same
|
|
56
|
+
* lever createFlow.ts uses to scope cross-scope writes. Without it the table,
|
|
57
|
+
* ACLs, and module land in the session user's default app. Normally === scopeSysId.
|
|
58
|
+
*/
|
|
59
|
+
transactionScopeSysId: string;
|
|
51
60
|
numberPrefix: string;
|
|
52
61
|
userRoleSysId: string;
|
|
53
62
|
userRoleLabel: string;
|
|
@@ -128,6 +128,16 @@ function applyTableSaveOverlay(base, o) {
|
|
|
128
128
|
f["sys_db_object.sys_scope_label"] = o.scopeLabel;
|
|
129
129
|
f["sys_display.sys_db_object.sys_scope"] = o.scopeLabel;
|
|
130
130
|
f["sys_display.original.sys_db_object.sys_scope"] = o.scopeLabel;
|
|
131
|
+
// NOTE: do NOT set sysparm_transaction_scope / sysparm_record_scope here. Setting
|
|
132
|
+
// them on a cross-scope POST triggers ServiceNow's scope-switch interstitial, which
|
|
133
|
+
// bounces the save to welcome.do (nothing created). The session's current app —
|
|
134
|
+
// switched via setCurrentApplication() before the POST — is what scopes the table.
|
|
135
|
+
// transactionScopeSysId is retained on OverlaySpec for callers that genuinely need
|
|
136
|
+
// it, but the orchestrator leaves it empty.
|
|
137
|
+
if (o.transactionScopeSysId) {
|
|
138
|
+
f["sysparm_transaction_scope"] = o.transactionScopeSysId;
|
|
139
|
+
f["sysparm_record_scope"] = o.transactionScopeSysId;
|
|
140
|
+
}
|
|
131
141
|
f["sys_db_object.number_ref.prefix"] = o.numberPrefix;
|
|
132
142
|
f["sys_db_object.create_access_controls"] = flag(o.createAccessControls);
|
|
133
143
|
f["ni.sys_db_object.create_access_controls"] = flag(o.createAccessControls);
|
|
@@ -9,19 +9,35 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Sequence (form-login replay, modeled on flowDesigner/createFlow.ts):
|
|
11
11
|
* 1. open a form session (login.do -> authenticated g_ck)
|
|
12
|
+
* 1b. switch the form session's current application to the target scope, so the
|
|
13
|
+
* new table is scoped correctly (the session app governs scope, NOT a field)
|
|
12
14
|
* 2. resolve sys_ids via the REST client (parent table, scope, role, app)
|
|
13
|
-
* 3. GET the new-record form, harvest its fields (ck, encoded_record
|
|
14
|
-
* 4. overlay the table + column values
|
|
15
|
-
*
|
|
15
|
+
* 3. GET the new-record form, harvest its fields (ck, encoded_record)
|
|
16
|
+
* 4. overlay the table + column values (columns ride a constant "Table Columns"
|
|
17
|
+
* relId — the new-record form doesn't render related lists to harvest one from)
|
|
18
|
+
* 5. POST -> expect 302 to sys_db_object.do?sys_id=<assigned>; parse that sys_id
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* VALIDATED LIVE 2026-06-13 (tenonworkstudio): a 6-column create landed the table,
|
|
21
|
+
* all columns, ACLs + role, the nav module, and 25 sys_update_xml rows in the pinned
|
|
22
|
+
* update set; a scoped insert round-tripped (physical table, not an orphan). The
|
|
23
|
+
* earlier defects (wrong scope, 0 columns, fabricated sys_id) are fixed here.
|
|
24
|
+
* ES6 only, no optional chaining, no `any`.
|
|
20
25
|
*/
|
|
21
26
|
import type { ServiceNowClient } from "../client";
|
|
22
27
|
import { ColumnSpec, AccessFlags } from "./buildTableSave";
|
|
23
28
|
/** Default parent for a custom scoped table — what Studio picks for "extends nothing". */
|
|
24
29
|
export declare var DEFAULT_SUPER_CLASS: string;
|
|
30
|
+
/**
|
|
31
|
+
* The "Table Columns" sys_relationship sys_id — the related list that carries the
|
|
32
|
+
* column XML in the `sys_db_object.REL:<relId>` list-edit key. It is a shipped OOB
|
|
33
|
+
* relationship (stable across instances), so we use it as a constant: the new-record
|
|
34
|
+
* (sys_id=-1) form does NOT render related lists, so the relId can't be harvested
|
|
35
|
+
* from it — relying on harvest discovery silently dropped every column. Override via
|
|
36
|
+
* params.columnsRelId only if an instance customized it.
|
|
37
|
+
*/
|
|
38
|
+
export declare var DEFAULT_COLUMNS_REL_ID: string;
|
|
39
|
+
/** Pull the assigned record sys_id out of a `...do?sys_id=<id>&...` 302 Location. */
|
|
40
|
+
export declare function parseSysIdFromLocation(location: string): string;
|
|
25
41
|
export interface CreateTableParams {
|
|
26
42
|
/** REST client for sys_id resolution + the update-set assertion. */
|
|
27
43
|
client: ServiceNowClient;
|
|
@@ -51,6 +67,10 @@ export interface CreateTableParams {
|
|
|
51
67
|
updateSetSysId?: string;
|
|
52
68
|
/** Override the Save UI-action sys_id (defaults to the well-known global Save). */
|
|
53
69
|
saveActionSysId?: string;
|
|
70
|
+
/** Override the "Table Columns" relationship sys_id (defaults to the OOB constant). */
|
|
71
|
+
columnsRelId?: string;
|
|
72
|
+
/** Emit diagnostic detail (harvested key, scope fields, response snippet) in the result note. */
|
|
73
|
+
debug?: boolean;
|
|
54
74
|
/** Instance/creds for the form session (default: env, same precedence as the client). */
|
|
55
75
|
instance?: string;
|
|
56
76
|
user?: string;
|
|
@@ -92,6 +112,12 @@ export interface CreateTableResult {
|
|
|
92
112
|
}
|
|
93
113
|
/** The well-known global "Save" UI action (sys_ui_action). Override per-instance if needed. */
|
|
94
114
|
export declare var DEFAULT_SAVE_ACTION: string;
|
|
95
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Project the record graph a real create emits (the documented 36-record shape for
|
|
117
|
+
* 12 cols + ACLs + role + menu). NOTE: a live create with Show-in-menu also re-emits
|
|
118
|
+
* the parent Application-Navigator menu into the update set, so the pinned set holds
|
|
119
|
+
* one MORE row than `total` (verified 2026-06-13: 6 cols → graph total 24, 25 rows
|
|
120
|
+
* captured). The extra row is the existing menu being touched, not a new record.
|
|
121
|
+
*/
|
|
96
122
|
export declare function projectTableGraph(columnCount: number, createAcls: boolean, hasRole: boolean): TableGraph;
|
|
97
123
|
export declare function createTable(params: CreateTableParams): Promise<CreateTableResult>;
|
|
@@ -10,14 +10,19 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Sequence (form-login replay, modeled on flowDesigner/createFlow.ts):
|
|
12
12
|
* 1. open a form session (login.do -> authenticated g_ck)
|
|
13
|
+
* 1b. switch the form session's current application to the target scope, so the
|
|
14
|
+
* new table is scoped correctly (the session app governs scope, NOT a field)
|
|
13
15
|
* 2. resolve sys_ids via the REST client (parent table, scope, role, app)
|
|
14
|
-
* 3. GET the new-record form, harvest its fields (ck, encoded_record
|
|
15
|
-
* 4. overlay the table + column values
|
|
16
|
-
*
|
|
16
|
+
* 3. GET the new-record form, harvest its fields (ck, encoded_record)
|
|
17
|
+
* 4. overlay the table + column values (columns ride a constant "Table Columns"
|
|
18
|
+
* relId — the new-record form doesn't render related lists to harvest one from)
|
|
19
|
+
* 5. POST -> expect 302 to sys_db_object.do?sys_id=<assigned>; parse that sys_id
|
|
17
20
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* VALIDATED LIVE 2026-06-13 (tenonworkstudio): a 6-column create landed the table,
|
|
22
|
+
* all columns, ACLs + role, the nav module, and 25 sys_update_xml rows in the pinned
|
|
23
|
+
* update set; a scoped insert round-tripped (physical table, not an orphan). The
|
|
24
|
+
* earlier defects (wrong scope, 0 columns, fabricated sys_id) are fixed here.
|
|
25
|
+
* ES6 only, no optional chaining, no `any`.
|
|
21
26
|
*/
|
|
22
27
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
28
|
if (k2 === undefined) k2 = k;
|
|
@@ -53,7 +58,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
53
58
|
};
|
|
54
59
|
})();
|
|
55
60
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
-
exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = void 0;
|
|
61
|
+
exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SUPER_CLASS = void 0;
|
|
62
|
+
exports.parseSysIdFromLocation = parseSysIdFromLocation;
|
|
57
63
|
exports.projectTableGraph = projectTableGraph;
|
|
58
64
|
exports.createTable = createTable;
|
|
59
65
|
const crypto = __importStar(require("crypto"));
|
|
@@ -62,12 +68,34 @@ const buildTableSave_1 = require("./buildTableSave");
|
|
|
62
68
|
const formSession_1 = require("./formSession");
|
|
63
69
|
/** Default parent for a custom scoped table — what Studio picks for "extends nothing". */
|
|
64
70
|
exports.DEFAULT_SUPER_CLASS = "sys_metadata";
|
|
71
|
+
/**
|
|
72
|
+
* The "Table Columns" sys_relationship sys_id — the related list that carries the
|
|
73
|
+
* column XML in the `sys_db_object.REL:<relId>` list-edit key. It is a shipped OOB
|
|
74
|
+
* relationship (stable across instances), so we use it as a constant: the new-record
|
|
75
|
+
* (sys_id=-1) form does NOT render related lists, so the relId can't be harvested
|
|
76
|
+
* from it — relying on harvest discovery silently dropped every column. Override via
|
|
77
|
+
* params.columnsRelId only if an instance customized it.
|
|
78
|
+
*/
|
|
79
|
+
exports.DEFAULT_COLUMNS_REL_ID = "4344f6f5bf1320001875647fcf0739ad";
|
|
80
|
+
/** Pull the assigned record sys_id out of a `...do?sys_id=<id>&...` 302 Location. */
|
|
81
|
+
function parseSysIdFromLocation(location) {
|
|
82
|
+
if (!location)
|
|
83
|
+
return "";
|
|
84
|
+
var m = String(location).match(/[?&]sys_id=([0-9a-f]{32})\b/i);
|
|
85
|
+
return m ? m[1] : "";
|
|
86
|
+
}
|
|
65
87
|
/** The well-known global "Save" UI action (sys_ui_action). Override per-instance if needed. */
|
|
66
88
|
exports.DEFAULT_SAVE_ACTION = "3dc6c898c3201100dcc2addbdfba8fe7";
|
|
67
89
|
function newSysId() {
|
|
68
90
|
return crypto.randomBytes(16).toString("hex");
|
|
69
91
|
}
|
|
70
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Project the record graph a real create emits (the documented 36-record shape for
|
|
94
|
+
* 12 cols + ACLs + role + menu). NOTE: a live create with Show-in-menu also re-emits
|
|
95
|
+
* the parent Application-Navigator menu into the update set, so the pinned set holds
|
|
96
|
+
* one MORE row than `total` (verified 2026-06-13: 6 cols → graph total 24, 25 rows
|
|
97
|
+
* captured). The extra row is the existing menu being touched, not a new record.
|
|
98
|
+
*/
|
|
71
99
|
function projectTableGraph(columnCount, createAcls, hasRole) {
|
|
72
100
|
var dictionary = columnCount + 1; // +1 collection row
|
|
73
101
|
var labels = columnCount + 1; // table label + per-column labels
|
|
@@ -155,17 +183,24 @@ async function createTable(params) {
|
|
|
155
183
|
roleSysId = role.sysId;
|
|
156
184
|
roleLabel = role.label || String(params.userRole);
|
|
157
185
|
}
|
|
186
|
+
// Resolve the app ALWAYS — it scopes the form session (setCurrentApplication),
|
|
187
|
+
// not just the optional nav module. scope may be a name or a sys_scope sys_id.
|
|
158
188
|
var appSysId = "";
|
|
159
189
|
var showInMenu = params.showInMenu === false ? false : true;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
190
|
+
var appQuery = SYS_ID.test(params.scope) ? "sys_id=" + params.scope : "scope=" + params.scope;
|
|
191
|
+
var apps = await client.table.query("sys_app", appQuery, 1);
|
|
192
|
+
if (apps.length > 0)
|
|
193
|
+
appSysId = String(apps[0].sys_id);
|
|
165
194
|
var saveAction = params.saveActionSysId && params.saveActionSysId.trim() ? params.saveActionSysId.trim() : exports.DEFAULT_SAVE_ACTION;
|
|
166
195
|
// 1: open the form session.
|
|
167
196
|
var auth = (0, formSession_1.resolveFormAuth)({ instance: params.instance, user: params.user, password: params.password });
|
|
168
197
|
var session = await (0, formSession_1.openFormSession)(auth);
|
|
198
|
+
// 1b: put the form session IN the target app so the new table is scoped correctly.
|
|
199
|
+
// Without this the table lands in the session user's default app (the #1 live defect).
|
|
200
|
+
var appSwitch = { ok: false, status: 0, body: "no app resolved" };
|
|
201
|
+
if (appSysId) {
|
|
202
|
+
appSwitch = await (0, formSession_1.setCurrentApplication)(auth, session, appSysId);
|
|
203
|
+
}
|
|
169
204
|
if (params.updateSetSysId) {
|
|
170
205
|
// Pin the REST session's update set; the form session inherits the user pref.
|
|
171
206
|
try {
|
|
@@ -176,6 +211,11 @@ async function createTable(params) {
|
|
|
176
211
|
// 3: harvest the new-record form.
|
|
177
212
|
var harvest = await (0, formSession_1.getNewRecordForm)(auth, session);
|
|
178
213
|
var tableSysId = newSysId();
|
|
214
|
+
// The new-record (sys_id=-1) form doesn't render related lists, so harvest.listEditKey
|
|
215
|
+
// is normally empty — fall back to the constant "Table Columns" relId so the columns
|
|
216
|
+
// always ride the POST. Relying on the harvest alone silently produced column-less tables.
|
|
217
|
+
var relId = params.columnsRelId && params.columnsRelId.trim() ? params.columnsRelId.trim() : exports.DEFAULT_COLUMNS_REL_ID;
|
|
218
|
+
var colKey = harvest.listEditKey ? harvest.listEditKey : (0, buildTableSave_1.listEditKey)(relId);
|
|
179
219
|
var overlay = {
|
|
180
220
|
name: params.name,
|
|
181
221
|
label: params.label,
|
|
@@ -185,6 +225,9 @@ async function createTable(params) {
|
|
|
185
225
|
superClassLabel: superClass.label,
|
|
186
226
|
scopeSysId: scopeRef.sysId,
|
|
187
227
|
scopeLabel: scopeRef.label,
|
|
228
|
+
// Left empty on purpose — the form-session app switch (1b) scopes the table.
|
|
229
|
+
// Setting these triggers a cross-scope interstitial that bounces to welcome.do.
|
|
230
|
+
transactionScopeSysId: "",
|
|
188
231
|
numberPrefix: params.numberPrefix ? params.numberPrefix : "",
|
|
189
232
|
userRoleSysId: roleSysId,
|
|
190
233
|
userRoleLabel: roleLabel,
|
|
@@ -193,16 +236,38 @@ async function createTable(params) {
|
|
|
193
236
|
accessFlags: params.accessFlags ? params.accessFlags : (0, buildTableSave_1.defaultAccessFlags)(),
|
|
194
237
|
selectedApplicationSysId: showInMenu ? appSysId : "",
|
|
195
238
|
menuName: params.label,
|
|
196
|
-
listEditKey:
|
|
239
|
+
listEditKey: colKey,
|
|
197
240
|
columnXml: columnXml
|
|
198
241
|
};
|
|
199
242
|
var fields = (0, buildTableSave_1.applyTableSaveOverlay)(harvest.fields, overlay);
|
|
200
243
|
// 4: POST the save.
|
|
201
244
|
var resp = await (0, formSession_1.postForm)(auth, session, "/sys_db_object.do", fields);
|
|
202
245
|
var ok = resp.status >= 300 && resp.status < 400; // 302 on success
|
|
246
|
+
// The form assigns its own sys_id for a new record — the 302 Location is the truth,
|
|
247
|
+
// not the sys_uniqueValue we sent. Prefer the parsed id; fall back to what we sent.
|
|
248
|
+
var assignedSysId = parseSysIdFromLocation(resp.location);
|
|
249
|
+
var finalSysId = ok ? (assignedSysId || tableSysId) : "";
|
|
250
|
+
var note;
|
|
251
|
+
if (ok) {
|
|
252
|
+
note = "Created via form save. Verify the " + graph.total
|
|
253
|
+
+ " records landed in scope " + scopeRef.sysId + " and the pinned update set.";
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
note = "save POST returned " + resp.status + " (expected 302). " + resp.body.slice(0, 200);
|
|
257
|
+
}
|
|
258
|
+
if (params.debug) {
|
|
259
|
+
note += " [debug: appSwitch=" + appSwitch.status + (appSwitch.ok ? "/ok" : "/FAIL:" + appSwitch.body)
|
|
260
|
+
+ " appSysId=" + (appSysId || "(none)")
|
|
261
|
+
+ " harvestedListEditKey=" + (harvest.listEditKey ? "yes" : "no")
|
|
262
|
+
+ " colKey=" + colKey
|
|
263
|
+
+ " fieldCount=" + Object.keys(fields).length
|
|
264
|
+
+ " location=" + resp.location
|
|
265
|
+
+ " sentSysId=" + tableSysId
|
|
266
|
+
+ " assignedSysId=" + (assignedSysId || "(unparsed)") + "]";
|
|
267
|
+
}
|
|
203
268
|
return {
|
|
204
269
|
status: ok ? "created" : "failed",
|
|
205
|
-
tableSysId:
|
|
270
|
+
tableSysId: finalSysId,
|
|
206
271
|
name: params.name,
|
|
207
272
|
label: params.label,
|
|
208
273
|
scopeSysId: scopeRef.sysId,
|
|
@@ -212,9 +277,6 @@ async function createTable(params) {
|
|
|
212
277
|
httpStatus: resp.status,
|
|
213
278
|
location: resp.location,
|
|
214
279
|
columnXml: columnXml,
|
|
215
|
-
note:
|
|
216
|
-
? "LIVE PATH NOT YET VALIDATED — verify the " + graph.total
|
|
217
|
-
+ " sys_update_xml rows landed in the pinned update set before trusting this result."
|
|
218
|
-
: "save POST returned " + resp.status + " (expected 302). " + resp.body.slice(0, 200)
|
|
280
|
+
note: note
|
|
219
281
|
};
|
|
220
282
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* buildColumnXml.ts. Uses Node 18+ global fetch (the package targets Node >=22).
|
|
9
9
|
* ES6 only, no optional chaining.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* in
|
|
11
|
+
* VALIDATED LIVE 2026-06-13 (tenonworkstudio). setCurrentApplication() puts the
|
|
12
|
+
* session in the target scope before the save (the scope-correctness lever).
|
|
13
13
|
*/
|
|
14
14
|
/** Resolved instance + credentials for a form session. */
|
|
15
15
|
export interface FormAuth {
|
|
@@ -32,6 +32,20 @@ export interface FormSession {
|
|
|
32
32
|
export declare function scrapeCk(html: string): string;
|
|
33
33
|
/** Form-login (login.do) and return an authenticated { ck, jar }. */
|
|
34
34
|
export declare function openFormSession(auth: FormAuth): Promise<FormSession>;
|
|
35
|
+
/**
|
|
36
|
+
* Switch the form session's CURRENT APPLICATION to `appSysId`. A new table's scope
|
|
37
|
+
* is governed by the session's current app, not a form field — and setting
|
|
38
|
+
* `sysparm_transaction_scope` on the POST triggers a cross-scope interstitial that
|
|
39
|
+
* bounces the save to welcome.do. The faithful lever is the same app-picker the UI
|
|
40
|
+
* drives: PUT the Next-Experience concourse picker, which updates both the session's
|
|
41
|
+
* in-memory current app and the `apps.current_app` user preference. Returns true if
|
|
42
|
+
* the picker accepted the switch. Best-effort: a non-2xx is reported, not thrown.
|
|
43
|
+
*/
|
|
44
|
+
export declare function setCurrentApplication(auth: FormAuth, session: FormSession, appSysId: string): Promise<{
|
|
45
|
+
ok: boolean;
|
|
46
|
+
status: number;
|
|
47
|
+
body: string;
|
|
48
|
+
}>;
|
|
35
49
|
/** The fields harvested from the new-record form, plus the discovered list-edit key. */
|
|
36
50
|
export interface HarvestedForm {
|
|
37
51
|
fields: Record<string, string>;
|
|
@@ -41,11 +55,10 @@ export interface HarvestedForm {
|
|
|
41
55
|
/**
|
|
42
56
|
* GET the new sys_db_object form and harvest every hidden/input field the browser
|
|
43
57
|
* would submit (sysparm_ck, sysparm_encoded_record, the dynamic `<sysid>_text`
|
|
44
|
-
* field, every sys_original.* default
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* documented field set; confirm against a real form during the spike.
|
|
58
|
+
* field, every sys_original.* default). The caller overlays capability params onto
|
|
59
|
+
* `fields` before POSTing. NOTE: the new-record (sys_id=-1) form does NOT render
|
|
60
|
+
* related lists, so `listEditKey` is normally empty — the caller falls back to the
|
|
61
|
+
* constant "Table Columns" relId. Validated live 2026-06-13.
|
|
49
62
|
*/
|
|
50
63
|
export declare function getNewRecordForm(auth: FormAuth, session: FormSession): Promise<HarvestedForm>;
|
|
51
64
|
/**
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* buildColumnXml.ts. Uses Node 18+ global fetch (the package targets Node >=22).
|
|
10
10
|
* ES6 only, no optional chaining.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* in
|
|
12
|
+
* VALIDATED LIVE 2026-06-13 (tenonworkstudio). setCurrentApplication() puts the
|
|
13
|
+
* session in the target scope before the save (the scope-correctness lever).
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.resolveFormAuth = resolveFormAuth;
|
|
17
17
|
exports.scrapeCk = scrapeCk;
|
|
18
18
|
exports.openFormSession = openFormSession;
|
|
19
|
+
exports.setCurrentApplication = setCurrentApplication;
|
|
19
20
|
exports.getNewRecordForm = getNewRecordForm;
|
|
20
21
|
exports.parseFormInputs = parseFormInputs;
|
|
21
22
|
exports.postForm = postForm;
|
|
@@ -94,14 +95,50 @@ async function openFormSession(auth) {
|
|
|
94
95
|
throw new Error("form login failed (no authenticated g_ck) — check SN_USER/SN_PASSWORD.");
|
|
95
96
|
return { ck: ck, jar: jar };
|
|
96
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Switch the form session's CURRENT APPLICATION to `appSysId`. A new table's scope
|
|
100
|
+
* is governed by the session's current app, not a form field — and setting
|
|
101
|
+
* `sysparm_transaction_scope` on the POST triggers a cross-scope interstitial that
|
|
102
|
+
* bounces the save to welcome.do. The faithful lever is the same app-picker the UI
|
|
103
|
+
* drives: PUT the Next-Experience concourse picker, which updates both the session's
|
|
104
|
+
* in-memory current app and the `apps.current_app` user preference. Returns true if
|
|
105
|
+
* the picker accepted the switch. Best-effort: a non-2xx is reported, not thrown.
|
|
106
|
+
*/
|
|
107
|
+
async function setCurrentApplication(auth, session, appSysId) {
|
|
108
|
+
if (!appSysId)
|
|
109
|
+
return { ok: false, status: 0, body: "no appSysId" };
|
|
110
|
+
var B = base(auth);
|
|
111
|
+
var res = await fetch(B + "/api/now/ui/concoursepicker/application", {
|
|
112
|
+
method: "PUT",
|
|
113
|
+
headers: {
|
|
114
|
+
"X-UserToken": session.ck,
|
|
115
|
+
Cookie: cookieHeader(session.jar),
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
Accept: "application/json"
|
|
118
|
+
},
|
|
119
|
+
// The picker's ApplicationProcessor expects `app_id` (a `value` body returns
|
|
120
|
+
// 400 "Missing Application Id").
|
|
121
|
+
body: JSON.stringify({ app_id: appSysId }),
|
|
122
|
+
redirect: "manual"
|
|
123
|
+
});
|
|
124
|
+
jarFrom(res, session.jar);
|
|
125
|
+
var body = "";
|
|
126
|
+
try {
|
|
127
|
+
body = await res.text();
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
body = "";
|
|
131
|
+
}
|
|
132
|
+
var ok = res.status >= 200 && res.status < 300;
|
|
133
|
+
return { ok: ok, status: res.status, body: body.slice(0, 200) };
|
|
134
|
+
}
|
|
97
135
|
/**
|
|
98
136
|
* GET the new sys_db_object form and harvest every hidden/input field the browser
|
|
99
137
|
* would submit (sysparm_ck, sysparm_encoded_record, the dynamic `<sysid>_text`
|
|
100
|
-
* field, every sys_original.* default
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* documented field set; confirm against a real form during the spike.
|
|
138
|
+
* field, every sys_original.* default). The caller overlays capability params onto
|
|
139
|
+
* `fields` before POSTing. NOTE: the new-record (sys_id=-1) form does NOT render
|
|
140
|
+
* related lists, so `listEditKey` is normally empty — the caller falls back to the
|
|
141
|
+
* constant "Table Columns" relId. Validated live 2026-06-13.
|
|
105
142
|
*/
|
|
106
143
|
async function getNewRecordForm(auth, session) {
|
|
107
144
|
var B = base(auth);
|
package/dist/table/index.d.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* sys_db_object.do save). See createTable.ts for the full sequence + the
|
|
4
4
|
* live-validation caveat.
|
|
5
5
|
*/
|
|
6
|
-
export { createTable, projectTableGraph, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION } from "./createTable";
|
|
6
|
+
export { createTable, projectTableGraph, parseSysIdFromLocation, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, DEFAULT_COLUMNS_REL_ID } from "./createTable";
|
|
7
7
|
export type { CreateTableParams, CreateTableResult, TableGraph } from "./createTable";
|
|
8
8
|
export { buildColumnXml, xmlEscape } from "./buildColumnXml";
|
|
9
9
|
export type { NormalizedColumn } from "./buildColumnXml";
|
|
10
10
|
export { normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, showInMenuKey, listEditKey, TYPE_MAP } from "./buildTableSave";
|
|
11
11
|
export type { ColumnSpec, AccessFlags, OverlaySpec } from "./buildTableSave";
|
|
12
|
-
export { resolveFormAuth, openFormSession, getNewRecordForm, parseFormInputs, postForm, scrapeCk } from "./formSession";
|
|
12
|
+
export { resolveFormAuth, openFormSession, setCurrentApplication, getNewRecordForm, parseFormInputs, postForm, scrapeCk } from "./formSession";
|
|
13
13
|
export type { FormAuth, FormSession, HarvestedForm, PostResult } from "./formSession";
|
package/dist/table/index.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* live-validation caveat.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = 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_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.projectTableGraph = exports.createTable = void 0;
|
|
8
|
+
exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = 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; } });
|
|
12
|
+
Object.defineProperty(exports, "parseSysIdFromLocation", { enumerable: true, get: function () { return createTable_1.parseSysIdFromLocation; } });
|
|
12
13
|
Object.defineProperty(exports, "DEFAULT_SUPER_CLASS", { enumerable: true, get: function () { return createTable_1.DEFAULT_SUPER_CLASS; } });
|
|
13
14
|
Object.defineProperty(exports, "DEFAULT_SAVE_ACTION", { enumerable: true, get: function () { return createTable_1.DEFAULT_SAVE_ACTION; } });
|
|
15
|
+
Object.defineProperty(exports, "DEFAULT_COLUMNS_REL_ID", { enumerable: true, get: function () { return createTable_1.DEFAULT_COLUMNS_REL_ID; } });
|
|
14
16
|
var buildColumnXml_1 = require("./buildColumnXml");
|
|
15
17
|
Object.defineProperty(exports, "buildColumnXml", { enumerable: true, get: function () { return buildColumnXml_1.buildColumnXml; } });
|
|
16
18
|
Object.defineProperty(exports, "xmlEscape", { enumerable: true, get: function () { return buildColumnXml_1.xmlEscape; } });
|
|
@@ -25,6 +27,7 @@ Object.defineProperty(exports, "TYPE_MAP", { enumerable: true, get: function ()
|
|
|
25
27
|
var formSession_1 = require("./formSession");
|
|
26
28
|
Object.defineProperty(exports, "resolveFormAuth", { enumerable: true, get: function () { return formSession_1.resolveFormAuth; } });
|
|
27
29
|
Object.defineProperty(exports, "openFormSession", { enumerable: true, get: function () { return formSession_1.openFormSession; } });
|
|
30
|
+
Object.defineProperty(exports, "setCurrentApplication", { enumerable: true, get: function () { return formSession_1.setCurrentApplication; } });
|
|
28
31
|
Object.defineProperty(exports, "getNewRecordForm", { enumerable: true, get: function () { return formSession_1.getNewRecordForm; } });
|
|
29
32
|
Object.defineProperty(exports, "parseFormInputs", { enumerable: true, get: function () { return formSession_1.parseFormInputs; } });
|
|
30
33
|
Object.defineProperty(exports, "postForm", { enumerable: true, get: function () { return formSession_1.postForm; } });
|