@jacebenson/jsn 3.0.1 → 3.0.3
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/package.json +1 -1
- package/src/app.js +8 -5
- package/src/cli.js +20 -1
- package/src/commands/dev/_generic.js +24 -2
- package/src/commands/dev/_simple.js +16 -4
- package/src/commands/dev/eval.js +2 -2
- package/src/commands/dev/flows.js +1 -1
- package/src/commands/dev/forms.js +4 -0
- package/src/commands/dev/lists.js +4 -0
- package/src/commands/dev/scopes.js +17 -7
- package/src/commands/dev/updatesets.js +51 -0
- package/src/commands/dev.js +13 -0
- package/src/commands/profiles.js +3 -0
- package/src/commands/records.js +28 -0
- package/src/commands/tickets.js +8 -8
- package/src/helpers.js +2 -0
- package/src/mutations.js +23 -2
- package/src/records/inspect.js +266 -0
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getEffectiveInstance } from './config.js';
|
|
|
7
7
|
import { extractProfileName } from './helpers.js';
|
|
8
8
|
import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
|
|
9
9
|
import { errUsage, errAuth } from './errors.js';
|
|
10
|
+
import { isMutationCommand } from './mutations.js';
|
|
10
11
|
import process from 'node:process';
|
|
11
12
|
|
|
12
13
|
|
|
@@ -57,7 +58,7 @@ export class App {
|
|
|
57
58
|
return getEffectiveInstance(this.config);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
async printContextHeader() {
|
|
61
|
+
async printContextHeader(argv = {}) {
|
|
61
62
|
if (!this.getEffectiveInstance() || !this.sdk) return;
|
|
62
63
|
if (process.env.JSN_NO_HEADER) return;
|
|
63
64
|
if (this.output.getFormat() === FormatJSON || this.output.getFormat() === FormatQuiet) return;
|
|
@@ -127,8 +128,10 @@ export class App {
|
|
|
127
128
|
|
|
128
129
|
process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
|
|
129
130
|
|
|
130
|
-
// ⚠️ Warning if in the Default update set
|
|
131
|
-
|
|
131
|
+
// ⚠️ Warning if in the Default update set (mutation commands only)
|
|
132
|
+
const profile = this.context.profileName ? this.config.profiles[this.context.profileName] : null;
|
|
133
|
+
const isYolo = profile?.yolo === true;
|
|
134
|
+
if (!isYolo && isMutationCommand(argv) && updateSet && updateSet.toLowerCase().includes('default')) {
|
|
132
135
|
const isGlobal = scope === 'global';
|
|
133
136
|
if (isGlobal) {
|
|
134
137
|
// 🔴 HARD WARNING — Global + Default
|
|
@@ -146,7 +149,7 @@ export class App {
|
|
|
146
149
|
'┃ Or switch to a scoped scope first: ┃\n' +
|
|
147
150
|
'┃ jsn dev scopes list ┃\n' +
|
|
148
151
|
'┃ ┃\n' +
|
|
149
|
-
'┃ (Run jsn updatesets yolo to silence this check)
|
|
152
|
+
'┃ (Run jsn dev updatesets yolo to silence this check) ┃\n' +
|
|
150
153
|
'┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\x1b[0m\n'
|
|
151
154
|
);
|
|
152
155
|
} else {
|
|
@@ -157,7 +160,7 @@ export class App {
|
|
|
157
160
|
' ⚠ Default update set in scope [' + scope + ']\n' +
|
|
158
161
|
' Changes are contained to this scope, but a named\n' +
|
|
159
162
|
' update set is still recommended for tracking.\n' +
|
|
160
|
-
' (Run \x1b[1mjsn updatesets yolo\x1b[22m to silence this warning)\n' +
|
|
163
|
+
' (Run \x1b[1mjsn dev updatesets yolo\x1b[22m to silence this warning)\n' +
|
|
161
164
|
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
162
165
|
'\x1b[0m' // reset
|
|
163
166
|
);
|
package/src/cli.js
CHANGED
|
@@ -156,7 +156,7 @@ export const cli = yargs(hideBin(process.argv))
|
|
|
156
156
|
|
|
157
157
|
// Print context header for interactive terminals (at the TOP, before command output)
|
|
158
158
|
if (!['help', 'version', 'completion', 'skill'].includes(cmd)) {
|
|
159
|
-
await argv.app.printContextHeader();
|
|
159
|
+
await argv.app.printContextHeader(argv);
|
|
160
160
|
}
|
|
161
161
|
})
|
|
162
162
|
.command(setupCmd(wrap))
|
|
@@ -185,6 +185,25 @@ export const cli = yargs(hideBin(process.argv))
|
|
|
185
185
|
if (err) throw err;
|
|
186
186
|
// No command given → show custom grouped help instead of yargs error
|
|
187
187
|
if (msg === 'You must specify a command') {
|
|
188
|
+
// Check if --profile/-p was used without a value — show profile info instead
|
|
189
|
+
const raw = process.argv.slice(2);
|
|
190
|
+
const hasLoneProfile = raw.some((a, i, arr) => {
|
|
191
|
+
if (a !== '--profile' && a !== '-p') return false;
|
|
192
|
+
const next = arr[i + 1];
|
|
193
|
+
return !next || next.startsWith('-');
|
|
194
|
+
});
|
|
195
|
+
if (hasLoneProfile) {
|
|
196
|
+
const cfg = loadConfig();
|
|
197
|
+
const activeName = cfg.activeProfile || cfg.defaultProfile;
|
|
198
|
+
process.stdout.write('Profiles:\n');
|
|
199
|
+
for (const [name, p] of Object.entries(cfg.profiles || {})) {
|
|
200
|
+
const marker = name === activeName ? ' *' : ' ';
|
|
201
|
+
process.stdout.write(` ${marker} ${name.padEnd(20)} ${p.instance_url || ''}\n`);
|
|
202
|
+
}
|
|
203
|
+
process.stdout.write('\nUsage: jsn --profile <name> <command>\n');
|
|
204
|
+
process.stdout.write(' jsn profiles use <name>\n');
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
188
207
|
process.stdout.write(renderHelp());
|
|
189
208
|
process.exit(0);
|
|
190
209
|
}
|
|
@@ -54,6 +54,28 @@ async function checkScope(sdk, recordScope) {
|
|
|
54
54
|
return { currentScope, recordScope };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the API scope name from a record's sys_scope reference field.
|
|
59
|
+
* With sysparm_display_value=all, sys_scope is { display_value: "Name", value: "<sys_id>" }.
|
|
60
|
+
* getStringField returns the display_value (human-readable), but we need the API name
|
|
61
|
+
* (e.g. "x_example_app") to compare against getCurrentScope().
|
|
62
|
+
*/
|
|
63
|
+
async function resolveRecordScope(sdk, record) {
|
|
64
|
+
const sysScope = record['sys_scope'];
|
|
65
|
+
let scopeId;
|
|
66
|
+
if (sysScope && typeof sysScope === 'object' && sysScope.value) {
|
|
67
|
+
scopeId = sysScope.value;
|
|
68
|
+
} else {
|
|
69
|
+
return getStringField(record, 'sys_scope');
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const scopeRecord = await sdk.get('sys_scope', scopeId);
|
|
73
|
+
return scopeRecord?.scope || 'global';
|
|
74
|
+
} catch {
|
|
75
|
+
return getStringField(record, 'sys_scope');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {}) {
|
|
58
80
|
const showFields = opts.showFields !== undefined ? opts.showFields : null;
|
|
59
81
|
const singular = toSingular(name, opts.singular);
|
|
@@ -257,7 +279,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
257
279
|
throw new Error(`${singular} not found: ${id}`);
|
|
258
280
|
}
|
|
259
281
|
const sysID = getStringField(records[0], 'sys_id');
|
|
260
|
-
const recordScope =
|
|
282
|
+
const recordScope = await resolveRecordScope(app.sdk, records[0]);
|
|
261
283
|
|
|
262
284
|
if (scopeValidation) {
|
|
263
285
|
const scopeErr = await checkScope(app.sdk, recordScope);
|
|
@@ -288,7 +310,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
288
310
|
}
|
|
289
311
|
const sysID = getStringField(records[0], 'sys_id');
|
|
290
312
|
const recordName = getStringField(records[0], 'name') || id;
|
|
291
|
-
const recordScope =
|
|
313
|
+
const recordScope = await resolveRecordScope(app.sdk, records[0]);
|
|
292
314
|
|
|
293
315
|
if (scopeValidation) {
|
|
294
316
|
const scopeErr = await checkScope(app.sdk, recordScope);
|
|
@@ -79,11 +79,23 @@ export const columnsCmd = (wrap) => buildDevCmd('columns', 'sys_dictionary', ['c
|
|
|
79
79
|
export const importCmd = (wrap) => buildDevCmd('import', 'sys_import_set', ['imports', 'imp'], ['sys_import_set', 'sys_import_row', 'sys_target_table', 'sys_target_sys_id'], wrap, { singular: 'import set', readOnly: true });
|
|
80
80
|
export const spPagesCmd = (wrap) => buildDevCmd('sppages', 'sp_page', ['sp-pages', 'pages'], ['id', 'title', 'sys_scope'], wrap, { singular: 'Service Portal page', readOnly: true });
|
|
81
81
|
export const spWidgetsCmd = (wrap) => buildDevCmd('spwidgets', 'sp_widget', ['sp-widget', 'widgets'], ['id', 'name', 'sys_scope'], wrap, { singular: 'Service Portal widget', readOnly: true });
|
|
82
|
-
export const uiPagesCmd = (wrap) => buildDevCmd('uipages', 'sys_ui_page', ['ui-page', 'pages'], ['name', 'sys_scope'], wrap, { singular: 'UI page',
|
|
83
|
-
export const appMenuCmd = (wrap) => buildDevCmd('appmenu', 'sys_app_application', ['app-menu', 'menu'], ['name', 'active', 'sys_scope'], wrap, { singular: 'application menu',
|
|
82
|
+
export const uiPagesCmd = (wrap) => buildDevCmd('uipages', 'sys_ui_page', ['ui-page', 'pages'], ['name', 'sys_scope'], wrap, { singular: 'UI page', scopeValidation: true });
|
|
83
|
+
export const appMenuCmd = (wrap) => buildDevCmd('appmenu', 'sys_app_application', ['app-menu', 'menu'], ['name', 'active', 'sys_scope'], wrap, { singular: 'application menu', scopeValidation: true });
|
|
84
84
|
export const scRAPICmd = (wrap) => buildDevCmd('scrapi', 'sys_ws_operation', ['scripted-rest', 'rest-api'], ['name', 'sys_ws_definition', 'sys_scope'], wrap, { singular: 'scripted REST API', readOnly: true });
|
|
85
85
|
|
|
86
86
|
// Commands with full CRUD
|
|
87
|
-
export const aclsCmd = (wrap) => buildDevCmd('acls', 'sys_security_acl', ['acl'], ['name', 'operation', 'type', 'active', 'sys_scope'], wrap, { singular: 'ACL',
|
|
87
|
+
export const aclsCmd = (wrap) => buildDevCmd('acls', 'sys_security_acl', ['acl'], ['name', 'operation', 'type', 'active', 'sys_scope'], wrap, { singular: 'ACL', scopeValidation: true });
|
|
88
88
|
export const rolesCmd = (wrap) => buildDevCmd('roles', 'sys_user_role', ['role', 'r'], ['name', 'description', 'elevated_privilege', 'sys_scope'], wrap, { singular: 'role', scopeValidation: true });
|
|
89
|
-
export const propertiesCmd = (wrap) => buildDevCmd('properties', 'sys_properties', ['property', 'prop'], ['name', 'value', 'description', 'sys_scope'], wrap, { singular: 'property',
|
|
89
|
+
export const propertiesCmd = (wrap) => buildDevCmd('properties', 'sys_properties', ['property', 'prop'], ['name', 'value', 'description', 'sys_scope'], wrap, { singular: 'property', scopeValidation: true });
|
|
90
|
+
|
|
91
|
+
// New commands for scoped-app tables
|
|
92
|
+
export const relationshipsCmd = (wrap) => buildDevCmd('relationships', 'sys_relationship', ['relationship', 'rel'], ['name', 'sys_scope'], wrap, { singular: 'relationship', scopeValidation: true });
|
|
93
|
+
export const appmodulesCmd = (wrap) => buildDevCmd('appmodules', 'sys_app_module', ['appmodule', 'am'], ['name', 'active', 'sys_scope'], wrap, { singular: 'application module', scopeValidation: true });
|
|
94
|
+
export const listcontrolsCmd = (wrap) => buildDevCmd('listcontrols', 'sys_ui_list_control', ['listcontrol', 'lc'], ['name', 'active', 'sys_scope'], wrap, { singular: 'list control', scopeValidation: true });
|
|
95
|
+
export const viewsCmd = (wrap) => buildDevCmd('views', 'sys_ui_view', ['view', 'vw'], ['name', 'title', 'sys_scope'], wrap, { singular: 'view', scopeValidation: true });
|
|
96
|
+
export const privilegesCmd = (wrap) => buildDevCmd('privileges', 'sys_scope_privilege', ['privilege', 'priv'], ['name', 'status', 'sys_scope'], wrap, { singular: 'privilege', scopeValidation: true });
|
|
97
|
+
export const securitytypesCmd = (wrap) => buildDevCmd('securitytypes', 'sys_security_type', ['securitytype', 'st'], ['name', 'active', 'sys_scope'], wrap, { singular: 'security type', scopeValidation: true });
|
|
98
|
+
export const uxscriptsCmd = (wrap) => buildDevCmd('uxscripts', 'sys_ux_lib_source_script', ['uxscript', 'ux'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UX script', scopeValidation: true });
|
|
99
|
+
export const aliasesCmd = (wrap) => buildDevCmd('aliases', 'sys_alias', ['alias', 'als'], ['name', 'table', 'sys_scope'], wrap, { singular: 'alias', scopeValidation: true });
|
|
100
|
+
export const catalogscriptsCmd = (wrap) => buildDevCmd('catalogscripts', 'catalog_script_client', ['catalogscript', 'cs'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog script', scopeValidation: true });
|
|
101
|
+
export const cataloguipoliciesCmd = (wrap) => buildDevCmd('cataloguipolicies', 'catalog_ui_policy', ['cataloguipolicy', 'cup'], ['short_description', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog UI policy', scopeValidation: true });
|
package/src/commands/dev/eval.js
CHANGED
|
@@ -6,8 +6,8 @@ export function evalCmd(wrap) {
|
|
|
6
6
|
describe: 'Execute background scripts on the instance',
|
|
7
7
|
builder: (yargs) => {
|
|
8
8
|
return yargs
|
|
9
|
-
.option('script', { alias: 's', type: 'string', describe:
|
|
10
|
-
.option('file', { alias: 'f', type: 'string', describe: 'Read script from file' });
|
|
9
|
+
.option('script', { alias: 's', type: 'string', describe: "JavaScript code to execute (single-quote the script, double quotes inside: --script 'gs.log(\"Hello\")')" })
|
|
10
|
+
.option('file', { alias: 'f', type: 'string', describe: 'Read script from file (avoids shell quoting issues)' });
|
|
11
11
|
},
|
|
12
12
|
handler: wrap(async (argv, app) => {
|
|
13
13
|
let script;
|
|
@@ -25,7 +25,7 @@ export function flowsCmd(wrap) {
|
|
|
25
25
|
formatLabel: r => `${getStringField(r, 'name')} ${getStringField(r, 'active') === 'true' ? '' : '[inactive]'}`,
|
|
26
26
|
});
|
|
27
27
|
if (picked) {
|
|
28
|
-
const inspection = await app.sdk.inspectFlow(picked
|
|
28
|
+
const inspection = await app.sdk.inspectFlow(getStringField(picked, 'sys_id'));
|
|
29
29
|
const formatted = formatFlowInspection(inspection, app.getEffectiveInstance());
|
|
30
30
|
return app.ok({ ...inspection, _formatted: formatted }, {
|
|
31
31
|
summary: `Flow: ${inspection.flow.name}`,
|
|
@@ -54,6 +54,10 @@ export function formsCmd(wrap) {
|
|
|
54
54
|
aliases: ['ls'],
|
|
55
55
|
describe: 'List form views for a table',
|
|
56
56
|
builder: (y) => y
|
|
57
|
+
.positional('table', {
|
|
58
|
+
describe: 'Table name (e.g. incident, change_request)',
|
|
59
|
+
type: 'string',
|
|
60
|
+
})
|
|
57
61
|
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
|
|
58
62
|
handler: wrap(async (argv, app) => {
|
|
59
63
|
const table = argv.table;
|
|
@@ -45,6 +45,10 @@ export function listsCmd(wrap) {
|
|
|
45
45
|
aliases: ['ls'],
|
|
46
46
|
describe: 'List list views for a table',
|
|
47
47
|
builder: (y) => y
|
|
48
|
+
.positional('table', {
|
|
49
|
+
describe: 'Table name (e.g. incident, change_request)',
|
|
50
|
+
type: 'string',
|
|
51
|
+
})
|
|
48
52
|
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
|
|
49
53
|
handler: wrap(async (argv, app) => {
|
|
50
54
|
const table = argv.table;
|
|
@@ -47,14 +47,24 @@ export function scopesCmd(wrap) {
|
|
|
47
47
|
.command({
|
|
48
48
|
command: 'show <scope>',
|
|
49
49
|
aliases: ['get'],
|
|
50
|
-
describe: 'Show a scope',
|
|
50
|
+
describe: 'Show a scope by scope value or name',
|
|
51
|
+
builder: (y) => y
|
|
52
|
+
.positional('scope', {
|
|
53
|
+
describe: 'Scope value (e.g. x_417611_daves_o_0) or display name',
|
|
54
|
+
type: 'string',
|
|
55
|
+
}),
|
|
51
56
|
handler: wrap(async (argv, app) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
let records;
|
|
58
|
+
// Try by scope value first, then by name
|
|
59
|
+
for (const queryField of ['scope', 'name']) {
|
|
60
|
+
const params = new URLSearchParams();
|
|
61
|
+
params.set('sysparm_query', `${queryField}=${argv.scope}`);
|
|
62
|
+
params.set('sysparm_limit', '1');
|
|
63
|
+
params.set('sysparm_display_value', 'all');
|
|
64
|
+
records = await app.sdk.list('sys_scope', params);
|
|
65
|
+
if (records.length > 0) break;
|
|
66
|
+
}
|
|
67
|
+
if (!records || records.length === 0) {
|
|
58
68
|
throw new Error(`Scope not found: ${argv.scope}`);
|
|
59
69
|
}
|
|
60
70
|
app.ok(records[0], { summary: `Scope ${argv.scope}` });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
3
|
|
|
3
4
|
export function updateSetsCmd(wrap) {
|
|
@@ -162,6 +163,55 @@ export function updateSetsCmd(wrap) {
|
|
|
162
163
|
});
|
|
163
164
|
}),
|
|
164
165
|
})
|
|
166
|
+
.command({
|
|
167
|
+
command: 'yolo',
|
|
168
|
+
aliases: ['silence'],
|
|
169
|
+
describe: 'Silence the "Default update set" warning for this profile',
|
|
170
|
+
handler: wrap(async (argv, app) => {
|
|
171
|
+
const name = app.context.profileName;
|
|
172
|
+
if (!name) {
|
|
173
|
+
throw new Error('No active profile to mark as yolo. Run jsn setup first.');
|
|
174
|
+
}
|
|
175
|
+
app.config.profiles[name].yolo = true;
|
|
176
|
+
const { saveConfig } = await import('../../config.js');
|
|
177
|
+
saveConfig(app.config);
|
|
178
|
+
app.ok({ profile: name, yolo: true }, { summary: 'Default update set warning silenced for this profile' });
|
|
179
|
+
}),
|
|
180
|
+
})
|
|
181
|
+
.command({
|
|
182
|
+
command: 'export <name>',
|
|
183
|
+
describe: 'Export an update set to XML',
|
|
184
|
+
builder: (y) => y
|
|
185
|
+
.positional('name', {
|
|
186
|
+
describe: 'Update set name',
|
|
187
|
+
type: 'string',
|
|
188
|
+
})
|
|
189
|
+
.option('output', {
|
|
190
|
+
alias: 'o',
|
|
191
|
+
type: 'string',
|
|
192
|
+
describe: 'Output file path (default: stdout)',
|
|
193
|
+
}),
|
|
194
|
+
handler: wrap(async (argv, app) => {
|
|
195
|
+
const params = new URLSearchParams();
|
|
196
|
+
params.set('sysparm_query', `name=${argv.name}`);
|
|
197
|
+
params.set('sysparm_limit', '1');
|
|
198
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
199
|
+
const records = await app.sdk.list('sys_update_set', params);
|
|
200
|
+
if (records.length === 0) {
|
|
201
|
+
throw new Error(`Update set not found: ${argv.name}`);
|
|
202
|
+
}
|
|
203
|
+
const sysID = getStringField(records[0], 'sys_id');
|
|
204
|
+
const instance = app.getEffectiveInstance();
|
|
205
|
+
const url = `${instance}/sys_update_set.do?XML&sysparm_sys_id=${sysID}`;
|
|
206
|
+
const xml = await app.sdk.rawRequest(url, { method: 'GET' });
|
|
207
|
+
if (argv.output) {
|
|
208
|
+
fs.writeFileSync(argv.output, xml, 'utf-8');
|
|
209
|
+
app.ok({ name: argv.name, sys_id: sysID, output: argv.output }, { summary: `Exported update set to ${argv.output}` });
|
|
210
|
+
} else {
|
|
211
|
+
process.stdout.write(xml + '\n');
|
|
212
|
+
}
|
|
213
|
+
}),
|
|
214
|
+
})
|
|
165
215
|
|
|
166
216
|
},
|
|
167
217
|
handler: (argv) => {
|
|
@@ -172,6 +222,7 @@ export function updateSetsCmd(wrap) {
|
|
|
172
222
|
console.log(' show <name> Show an update set');
|
|
173
223
|
console.log(' set <name> Set the current update set');
|
|
174
224
|
console.log(' create Create a new update set (auto-sets as current)');
|
|
225
|
+
console.log(' export <name> Export an update set to XML');
|
|
175
226
|
console.log(' complete <name> Mark an update set as complete (coming soon)');
|
|
176
227
|
console.log(' yolo Silence the "Default update set" warning');
|
|
177
228
|
console.log('\nRun "jsn dev updatesets <command> --help" for details.');
|
package/src/commands/dev.js
CHANGED
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
tablesCmd, columnsCmd, importCmd,
|
|
7
7
|
spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd, scRAPICmd,
|
|
8
8
|
aclsCmd, rolesCmd, propertiesCmd,
|
|
9
|
+
relationshipsCmd, appmodulesCmd, listcontrolsCmd, viewsCmd,
|
|
10
|
+
privilegesCmd, securitytypesCmd, uxscriptsCmd, aliasesCmd,
|
|
11
|
+
catalogscriptsCmd, cataloguipoliciesCmd,
|
|
9
12
|
} from './dev/_simple.js';
|
|
10
13
|
import { flowsCmd } from './dev/flows.js';
|
|
11
14
|
import { formsCmd } from './dev/forms.js';
|
|
@@ -44,6 +47,16 @@ export function devCmd(wrap) {
|
|
|
44
47
|
.command(updateSetsCmd(wrap))
|
|
45
48
|
.command(scopesCmd(wrap))
|
|
46
49
|
.command(propertiesCmd(wrap))
|
|
50
|
+
.command(relationshipsCmd(wrap))
|
|
51
|
+
.command(appmodulesCmd(wrap))
|
|
52
|
+
.command(listcontrolsCmd(wrap))
|
|
53
|
+
.command(viewsCmd(wrap))
|
|
54
|
+
.command(privilegesCmd(wrap))
|
|
55
|
+
.command(securitytypesCmd(wrap))
|
|
56
|
+
.command(uxscriptsCmd(wrap))
|
|
57
|
+
.command(aliasesCmd(wrap))
|
|
58
|
+
.command(catalogscriptsCmd(wrap))
|
|
59
|
+
.command(cataloguipoliciesCmd(wrap))
|
|
47
60
|
.command(logsCmd(wrap))
|
|
48
61
|
.command(evalCmd(wrap))
|
|
49
62
|
.command(restCmd(wrap));
|
package/src/commands/profiles.js
CHANGED
|
@@ -181,6 +181,9 @@ export function profilesCmd(wrap) {
|
|
|
181
181
|
console.log(' use <name> Switch to a different profile');
|
|
182
182
|
console.log(' remove <name> Remove a profile');
|
|
183
183
|
console.log('\nRun "jsn profiles <command> --help" for more details.');
|
|
184
|
+
console.log('\nNote: You can also create a profile by authenticating:');
|
|
185
|
+
console.log(' jsn auth login --profile <name> <instance-url>');
|
|
186
|
+
console.log(' jsn auth login --profile staging --password https://dev12345.service-now.com');
|
|
184
187
|
}
|
|
185
188
|
},
|
|
186
189
|
};
|
package/src/commands/records.js
CHANGED
|
@@ -165,6 +165,34 @@ export function recordsCmd(wrap) {
|
|
|
165
165
|
app.ok({ message: 'Record deleted', table: argv.table, sys_id: argv['sys-id'] }, { summary: `Deleted record from ${argv.table}` });
|
|
166
166
|
}),
|
|
167
167
|
})
|
|
168
|
+
.command({
|
|
169
|
+
command: 'inspect <table> <identifier>',
|
|
170
|
+
aliases: ['debug', 'diag'],
|
|
171
|
+
describe: 'Inspect a record: show audit history, business rules, and running flows',
|
|
172
|
+
builder: (y) => y
|
|
173
|
+
.positional('table', { describe: 'Table name (e.g. incident)', type: 'string' })
|
|
174
|
+
.positional('identifier', { describe: 'Record sys_id or number', type: 'string' }),
|
|
175
|
+
handler: wrap(async (argv, app) => {
|
|
176
|
+
const { inspectRecord, formatInspectOutput } = await import('../records/inspect.js');
|
|
177
|
+
const result = await inspectRecord(app, argv.table, argv.identifier);
|
|
178
|
+
const breadcrumbs = [
|
|
179
|
+
...result.flows.map(f => ({
|
|
180
|
+
action: 'show',
|
|
181
|
+
cmd: `jsn dev flows show "${f.flow}"`,
|
|
182
|
+
description: `View flow details for ${f.flow}`,
|
|
183
|
+
})),
|
|
184
|
+
...result.businessRules.slice(0, 5).map(br => ({
|
|
185
|
+
action: 'show',
|
|
186
|
+
cmd: `jsn dev rules show "${br.name}"`,
|
|
187
|
+
description: `View business rule: ${br.name}`,
|
|
188
|
+
})),
|
|
189
|
+
];
|
|
190
|
+
app.ok({ ...result, _formatted: formatInspectOutput(result) }, {
|
|
191
|
+
summary: `Inspected ${argv.table} ${argv.identifier}`,
|
|
192
|
+
breadcrumbs,
|
|
193
|
+
});
|
|
194
|
+
}),
|
|
195
|
+
})
|
|
168
196
|
|
|
169
197
|
},
|
|
170
198
|
handler: () => {},
|
package/src/commands/tickets.js
CHANGED
|
@@ -26,7 +26,7 @@ export function ticketsCmd(wrap) {
|
|
|
26
26
|
if (fields) params.set('sysparm_fields', fields);
|
|
27
27
|
const q = argv.query ? argv.query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
|
|
28
28
|
params.set('sysparm_query', q);
|
|
29
|
-
const records = await app.sdk.list('
|
|
29
|
+
const records = await app.sdk.list('task', params);
|
|
30
30
|
const displayRecords = fields ? records.map(r => formatRecordForDisplay(r, columns)) : records;
|
|
31
31
|
const breadcrumbs = [
|
|
32
32
|
{ action: 'create', cmd: 'jsn tickets create --data \'{...}\'', description: 'Create a new ticket' },
|
|
@@ -46,7 +46,7 @@ export function ticketsCmd(wrap) {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
app.ok({
|
|
49
|
-
table: '
|
|
49
|
+
table: 'task',
|
|
50
50
|
count: records.length,
|
|
51
51
|
columns,
|
|
52
52
|
records: displayRecords,
|
|
@@ -64,7 +64,7 @@ export function ticketsCmd(wrap) {
|
|
|
64
64
|
params.set('sysparm_query', `number=${argv.number}`);
|
|
65
65
|
params.set('sysparm_limit', '1');
|
|
66
66
|
params.set('sysparm_display_value', 'all');
|
|
67
|
-
const records = await app.sdk.list('
|
|
67
|
+
const records = await app.sdk.list('task', params);
|
|
68
68
|
if (records.length === 0) {
|
|
69
69
|
throw new Error(`Ticket not found: ${argv.number}`);
|
|
70
70
|
}
|
|
@@ -77,7 +77,7 @@ export function ticketsCmd(wrap) {
|
|
|
77
77
|
builder: (y) => y.option('data', { type: 'string', demandOption: true, describe: 'JSON fields (e.g. \'{"state":"2","priority":"1"}\')' }),
|
|
78
78
|
handler: wrap(async (argv, app) => {
|
|
79
79
|
const recordData = JSON.parse(argv.data);
|
|
80
|
-
const record = await app.sdk.create('
|
|
80
|
+
const record = await app.sdk.create('task', recordData);
|
|
81
81
|
app.ok(record, { summary: `Created ticket ${getStringField(record, 'number')}` });
|
|
82
82
|
}),
|
|
83
83
|
})
|
|
@@ -90,12 +90,12 @@ export function ticketsCmd(wrap) {
|
|
|
90
90
|
const findParams = new URLSearchParams();
|
|
91
91
|
findParams.set('sysparm_query', `number=${argv.number}`);
|
|
92
92
|
findParams.set('sysparm_limit', '1');
|
|
93
|
-
const records = await app.sdk.list('
|
|
93
|
+
const records = await app.sdk.list('task', findParams);
|
|
94
94
|
if (records.length === 0) {
|
|
95
95
|
throw new Error(`Ticket not found: ${argv.number}`);
|
|
96
96
|
}
|
|
97
97
|
const sysID = getStringField(records[0], 'sys_id');
|
|
98
|
-
const updated = await app.sdk.update('
|
|
98
|
+
const updated = await app.sdk.update('task', sysID, recordData);
|
|
99
99
|
app.ok(updated, { summary: `Updated ticket ${argv.number}` });
|
|
100
100
|
}),
|
|
101
101
|
})
|
|
@@ -106,12 +106,12 @@ export function ticketsCmd(wrap) {
|
|
|
106
106
|
const findParams = new URLSearchParams();
|
|
107
107
|
findParams.set('sysparm_query', `number=${argv.number}`);
|
|
108
108
|
findParams.set('sysparm_limit', '1');
|
|
109
|
-
const records = await app.sdk.list('
|
|
109
|
+
const records = await app.sdk.list('task', findParams);
|
|
110
110
|
if (records.length === 0) {
|
|
111
111
|
throw new Error(`Ticket not found: ${argv.number}`);
|
|
112
112
|
}
|
|
113
113
|
const sysID = getStringField(records[0], 'sys_id');
|
|
114
|
-
await app.sdk.delete('
|
|
114
|
+
await app.sdk.delete('task', sysID);
|
|
115
115
|
app.ok({ number: argv.number, message: 'Ticket deleted' }, { summary: `Deleted ticket ${argv.number}` });
|
|
116
116
|
}),
|
|
117
117
|
})
|
package/src/helpers.js
CHANGED
|
@@ -143,6 +143,8 @@ export function parseDataArg(argv) {
|
|
|
143
143
|
let raw;
|
|
144
144
|
if (argv['data-file']) {
|
|
145
145
|
raw = fs.readFileSync(argv['data-file'], 'utf-8');
|
|
146
|
+
// Strip UTF-8 BOM (\ufeff) which some editors (Windows/PowerShell) add
|
|
147
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
146
148
|
} else if (argv.data) {
|
|
147
149
|
raw = argv.data;
|
|
148
150
|
} else {
|
package/src/mutations.js
CHANGED
|
@@ -23,11 +23,32 @@ export const MUTATION_COMMANDS = [
|
|
|
23
23
|
['tasks', 'create'],
|
|
24
24
|
['tasks', 'update'],
|
|
25
25
|
['tasks', 'delete'],
|
|
26
|
-
//
|
|
26
|
+
// Tickets
|
|
27
|
+
['tickets', 'create'],
|
|
28
|
+
['tickets', 'update'],
|
|
29
|
+
['tickets', 'delete'],
|
|
30
|
+
// Users
|
|
31
|
+
['users', 'create'],
|
|
32
|
+
['users', 'update'],
|
|
33
|
+
['users', 'delete'],
|
|
34
|
+
// Groups
|
|
35
|
+
['groups', 'create'],
|
|
36
|
+
['groups', 'update'],
|
|
37
|
+
['groups', 'delete'],
|
|
38
|
+
// Catalog
|
|
39
|
+
['catalog', 'create-item'],
|
|
40
|
+
// Dev flows
|
|
41
|
+
['dev', 'flows', 'create'],
|
|
42
|
+
['dev', 'flows', 'update'],
|
|
43
|
+
['dev', 'flows', 'delete'],
|
|
44
|
+
// Dev scopes
|
|
45
|
+
['dev', 'scopes', 'create'],
|
|
46
|
+
['dev', 'scopes', 'set'],
|
|
47
|
+
// Dev eval
|
|
27
48
|
['dev', 'eval'],
|
|
49
|
+
// Dev updatesets
|
|
28
50
|
['dev', 'updatesets', 'set'],
|
|
29
51
|
['dev', 'updatesets', 'create'],
|
|
30
|
-
['dev', 'scopes', 'set'],
|
|
31
52
|
];
|
|
32
53
|
|
|
33
54
|
/**
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { isHexString, getStringField } from '../helpers.js';
|
|
2
|
+
|
|
3
|
+
export async function resolveIdentifier(app, table, identifier) {
|
|
4
|
+
// If it looks like a sys_id (32 hex chars), use it directly
|
|
5
|
+
if (isHexString(identifier) && identifier.length === 32) {
|
|
6
|
+
return identifier;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Otherwise, assume it's a record number — query the table
|
|
10
|
+
const params = new URLSearchParams();
|
|
11
|
+
params.set('sysparm_query', `number=${identifier}`);
|
|
12
|
+
params.set('sysparm_limit', '1');
|
|
13
|
+
params.set('sysparm_fields', 'sys_id');
|
|
14
|
+
const records = await app.sdk.list(table, params);
|
|
15
|
+
if (records.length === 0) {
|
|
16
|
+
throw new Error(`Record not found in ${table}: ${identifier}`);
|
|
17
|
+
}
|
|
18
|
+
return getStringField(records[0], 'sys_id');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function fetchHistory(app, table, sysId, limit = 20) {
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
params.set('sysparm_query', `documentkey=${sysId}^tablename=${table}`);
|
|
24
|
+
params.set('sysparm_limit', String(limit));
|
|
25
|
+
params.set('sysparm_fields', 'fieldname,newvalue,oldvalue,sys_created_on,sys_created_by');
|
|
26
|
+
params.set('sysparm_display_value', 'all');
|
|
27
|
+
params.set('sysparm_order_by', 'sys_created_on');
|
|
28
|
+
const records = await app.sdk.list('sys_audit', params);
|
|
29
|
+
return records.map(r => ({
|
|
30
|
+
field: r.fieldname?.display_value || r.fieldname,
|
|
31
|
+
oldValue: (r.oldvalue?.display_value ?? r.oldvalue?.value ?? '') || '',
|
|
32
|
+
newValue: (r.newvalue?.display_value ?? r.newvalue?.value ?? '') || '',
|
|
33
|
+
_oldRaw: r.oldvalue?.value ?? r.oldvalue ?? '',
|
|
34
|
+
_newRaw: r.newvalue?.value ?? r.newvalue ?? '',
|
|
35
|
+
changedOn: r.sys_created_on?.display_value || r.sys_created_on,
|
|
36
|
+
changedBy: r.sys_created_by?.display_value || r.sys_created_by,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Known choice fields and their display labels from sys_choice
|
|
41
|
+
const CHOICE_FIELDS = new Set(['state', 'incident_state', 'priority', 'impact', 'urgency', 'active', 'category', 'subcategory']);
|
|
42
|
+
|
|
43
|
+
// Known sys_user reference fields
|
|
44
|
+
const USER_REF_FIELDS = new Set(['closed_by', 'assigned_to', 'opened_by', 'resolved_by', 'called_by', 'caller_id']);
|
|
45
|
+
|
|
46
|
+
// Boolean fields stored as 0/1 in sys_audit
|
|
47
|
+
const BOOL_FIELDS = new Set(['active']);
|
|
48
|
+
|
|
49
|
+
async function resolveHistoryDisplayValues(app, table, history) {
|
|
50
|
+
// Collect unique sys_ids for user reference resolution
|
|
51
|
+
const userSysIds = new Set();
|
|
52
|
+
for (const h of history) {
|
|
53
|
+
if (USER_REF_FIELDS.has(h.field)) {
|
|
54
|
+
if (isHexString(h.oldValue) && h.oldValue.length === 32) userSysIds.add(h.oldValue);
|
|
55
|
+
if (isHexString(h.newValue) && h.newValue.length === 32) userSysIds.add(h.newValue);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Batch lookup user names
|
|
60
|
+
const userNameMap = new Map();
|
|
61
|
+
if (userSysIds.size > 0) {
|
|
62
|
+
const idList = [...userSysIds].join(',');
|
|
63
|
+
const params = new URLSearchParams();
|
|
64
|
+
params.set('sysparm_query', `sys_idIN${idList}`);
|
|
65
|
+
params.set('sysparm_limit', '100');
|
|
66
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
67
|
+
params.set('sysparm_display_value', 'all');
|
|
68
|
+
try {
|
|
69
|
+
const users = await app.sdk.list('sys_user', params);
|
|
70
|
+
for (const u of users) {
|
|
71
|
+
const uid = getStringField(u, 'sys_id');
|
|
72
|
+
const name = u.name?.display_value || u.name;
|
|
73
|
+
if (uid && name) userNameMap.set(uid, name);
|
|
74
|
+
}
|
|
75
|
+
} catch { /* non-fatal */ }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Collect unique (field, value) pairs for choice resolution
|
|
79
|
+
const choiceLookups = new Set();
|
|
80
|
+
for (const h of history) {
|
|
81
|
+
if (CHOICE_FIELDS.has(h.field)) {
|
|
82
|
+
if (h.oldValue && h.oldValue !== '_raw_old_') choiceLookups.add(`${h.field}|${h.oldValue}`);
|
|
83
|
+
if (h.newValue && h.newValue !== '_raw_new_') choiceLookups.add(`${h.field}|${h.newValue}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Batch lookup choice labels
|
|
88
|
+
const choiceLabelMap = new Map();
|
|
89
|
+
if (choiceLookups.size > 0) {
|
|
90
|
+
for (const key of choiceLookups) {
|
|
91
|
+
const [field, val] = key.split('|');
|
|
92
|
+
const params = new URLSearchParams();
|
|
93
|
+
params.set('sysparm_query', `name=${table}^element=${field}^value=${val}^language=en`);
|
|
94
|
+
params.set('sysparm_limit', '1');
|
|
95
|
+
params.set('sysparm_fields', 'value,label');
|
|
96
|
+
params.set('sysparm_display_value', 'all');
|
|
97
|
+
try {
|
|
98
|
+
const choices = await app.sdk.list('sys_choice', params);
|
|
99
|
+
if (choices.length > 0) {
|
|
100
|
+
const label = choices[0].label?.display_value || choices[0].label;
|
|
101
|
+
if (label) choiceLabelMap.set(key, label);
|
|
102
|
+
}
|
|
103
|
+
} catch { /* non-fatal */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Apply resolutions to history entries
|
|
108
|
+
return history.map(h => {
|
|
109
|
+
let oldDisplay = h.oldValue;
|
|
110
|
+
let newDisplay = h.newValue;
|
|
111
|
+
|
|
112
|
+
// Resolve user references
|
|
113
|
+
if (USER_REF_FIELDS.has(h.field)) {
|
|
114
|
+
if (isHexString(oldDisplay) && oldDisplay.length === 32) {
|
|
115
|
+
oldDisplay = userNameMap.get(oldDisplay) || oldDisplay || '(empty)';
|
|
116
|
+
}
|
|
117
|
+
if (isHexString(newDisplay) && newDisplay.length === 32) {
|
|
118
|
+
newDisplay = userNameMap.get(newDisplay) || newDisplay || '(empty)';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Resolve choice labels
|
|
123
|
+
if (CHOICE_FIELDS.has(h.field)) {
|
|
124
|
+
const oldLabel = choiceLabelMap.get(`${h.field}|${h.oldValue}`);
|
|
125
|
+
const newLabel = choiceLabelMap.get(`${h.field}|${h.newValue}`);
|
|
126
|
+
if (oldLabel) oldDisplay = oldLabel;
|
|
127
|
+
if (newLabel) newDisplay = newLabel;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Resolve boolean fields
|
|
131
|
+
if (BOOL_FIELDS.has(h.field)) {
|
|
132
|
+
if (oldDisplay === '1') oldDisplay = 'Yes';
|
|
133
|
+
else if (oldDisplay === '0') oldDisplay = 'No';
|
|
134
|
+
if (newDisplay === '1') newDisplay = 'Yes';
|
|
135
|
+
else if (newDisplay === '0') newDisplay = 'No';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { ...h, oldValue: oldDisplay || '(empty)', newValue: newDisplay || '(empty)' };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function fetchBusinessRules(app, table) {
|
|
143
|
+
const params = new URLSearchParams();
|
|
144
|
+
params.set('sysparm_query', `collection=${table}^active=true`);
|
|
145
|
+
params.set('sysparm_limit', '50');
|
|
146
|
+
params.set('sysparm_fields', 'name,collection,order,active,sys_scope,when,filter_condition');
|
|
147
|
+
params.set('sysparm_display_value', 'all');
|
|
148
|
+
params.set('sysparm_order_by', 'order');
|
|
149
|
+
const records = await app.sdk.list('sys_script', params);
|
|
150
|
+
return records
|
|
151
|
+
.map(r => ({
|
|
152
|
+
name: r.name?.display_value || r.name,
|
|
153
|
+
order: r.order?.display_value || r.order,
|
|
154
|
+
scope: r.sys_scope?.display_value || r.sys_scope || 'global',
|
|
155
|
+
when: r.when?.display_value || r.when || '',
|
|
156
|
+
condition: r.filter_condition?.display_value || r.filter_condition?.value || '',
|
|
157
|
+
}))
|
|
158
|
+
.sort((a, b) => {
|
|
159
|
+
const aOrder = parseInt(String(a.order).replace(/,/g, ''), 10) || 0;
|
|
160
|
+
const bOrder = parseInt(String(b.order).replace(/,/g, ''), 10) || 0;
|
|
161
|
+
return aOrder - bOrder;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function fetchFlows(app, sysId) {
|
|
166
|
+
const params = new URLSearchParams();
|
|
167
|
+
params.set('sysparm_query', `source_record=${sysId}`);
|
|
168
|
+
params.set('sysparm_limit', '20');
|
|
169
|
+
params.set('sysparm_fields', 'flow_catalog_model,name,execution_id,state,engine_major_version,sys_created_on,origins,calling_source');
|
|
170
|
+
params.set('sysparm_display_value', 'all');
|
|
171
|
+
try {
|
|
172
|
+
const records = await app.sdk.list('sys_flow_context', params);
|
|
173
|
+
return records.map(r => ({
|
|
174
|
+
flow: r.name?.display_value || r.name || r.flow_catalog_model?.display_value || r.flow_catalog_model?.value || '(deleted flow)',
|
|
175
|
+
executionId: r.execution_id?.display_value || r.execution_id,
|
|
176
|
+
state: r.state?.display_value || r.state,
|
|
177
|
+
version: r.engine_major_version?.display_value || r.engine_major_version,
|
|
178
|
+
started: r.sys_created_on?.display_value || r.sys_created_on,
|
|
179
|
+
}));
|
|
180
|
+
} catch {
|
|
181
|
+
return []; // Flow Designer might not be installed
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function formatInspectOutput(data) {
|
|
186
|
+
const lines = [];
|
|
187
|
+
lines.push('');
|
|
188
|
+
lines.push(`\u{1F4CB} ${data.table} ${data.sys_id}`);
|
|
189
|
+
lines.push('');
|
|
190
|
+
|
|
191
|
+
// History section
|
|
192
|
+
lines.push('\u25B6 HISTORY');
|
|
193
|
+
lines.push('\u2500'.repeat(50));
|
|
194
|
+
if (data.history.length === 0) {
|
|
195
|
+
lines.push(' (no audit history found)');
|
|
196
|
+
} else {
|
|
197
|
+
for (const h of data.history.slice(0, 10)) {
|
|
198
|
+
lines.push(` ${h.changedOn} ${h.changedBy} ${h.field}: ${h.oldValue || '(empty)'} \u2192 ${h.newValue}`);
|
|
199
|
+
}
|
|
200
|
+
if (data.history.length > 10) {
|
|
201
|
+
lines.push(` ... and ${data.history.length - 10} more`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lines.push('');
|
|
205
|
+
|
|
206
|
+
// Business rules section
|
|
207
|
+
lines.push('\u25B6 BUSINESS RULES');
|
|
208
|
+
lines.push('\u2500'.repeat(50));
|
|
209
|
+
if (data.businessRules.length === 0) {
|
|
210
|
+
lines.push(' (no active business rules on this table)');
|
|
211
|
+
} else {
|
|
212
|
+
// Group by execution phase: before → display → after → async
|
|
213
|
+
const phases = ['before', 'display', 'after', 'async'];
|
|
214
|
+
const grouped = {};
|
|
215
|
+
for (const br of data.businessRules) {
|
|
216
|
+
const phase = br.when?.toLowerCase() || '';
|
|
217
|
+
if (!grouped[phase]) grouped[phase] = [];
|
|
218
|
+
grouped[phase].push(br);
|
|
219
|
+
}
|
|
220
|
+
for (const phase of phases) {
|
|
221
|
+
const rules = grouped[phase]?.sort((a, b) => {
|
|
222
|
+
const aOrder = parseInt(String(a.order).replace(/,/g, ''), 10) || 0;
|
|
223
|
+
const bOrder = parseInt(String(b.order).replace(/,/g, ''), 10) || 0;
|
|
224
|
+
return aOrder - bOrder;
|
|
225
|
+
});
|
|
226
|
+
if (!rules || rules.length === 0) continue;
|
|
227
|
+
lines.push(` ${phase.charAt(0).toUpperCase() + phase.slice(1)}:`);
|
|
228
|
+
for (const br of rules) {
|
|
229
|
+
lines.push(` [${br.order}] ${br.name}${br.scope !== 'global' ? ` (${br.scope})` : ''}`);
|
|
230
|
+
if (br.condition) {
|
|
231
|
+
const cond = br.condition.length > 80 ? br.condition.substring(0, 77) + '...' : br.condition;
|
|
232
|
+
lines.push(` Condition: ${cond}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
lines.push('');
|
|
238
|
+
|
|
239
|
+
// Flows section
|
|
240
|
+
lines.push('\u25B6 RUNNING FLOWS');
|
|
241
|
+
lines.push('\u2500'.repeat(50));
|
|
242
|
+
if (data.flows.length === 0) {
|
|
243
|
+
lines.push(' (no running flows for this record)');
|
|
244
|
+
} else {
|
|
245
|
+
for (const f of data.flows) {
|
|
246
|
+
lines.push(` Flow: ${f.flow}`);
|
|
247
|
+
lines.push(` Status: ${f.state} | Version: ${f.version}`);
|
|
248
|
+
if (f.started) lines.push(` Started: ${f.started}`);
|
|
249
|
+
lines.push('');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
lines.push('');
|
|
253
|
+
|
|
254
|
+
return lines.join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function inspectRecord(app, table, identifier) {
|
|
258
|
+
const sysId = await resolveIdentifier(app, table, identifier);
|
|
259
|
+
const [rawHistory, businessRules, flows] = await Promise.all([
|
|
260
|
+
fetchHistory(app, table, sysId),
|
|
261
|
+
fetchBusinessRules(app, table),
|
|
262
|
+
fetchFlows(app, sysId),
|
|
263
|
+
]);
|
|
264
|
+
const history = await resolveHistoryDisplayValues(app, table, rawHistory);
|
|
265
|
+
return { table, sys_id: sysId, history, businessRules, flows };
|
|
266
|
+
}
|