@jacebenson/jsn 4.2.4 → 4.3.1
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 +12 -0
- package/package.json +2 -2
- package/src/app.js +9 -0
- package/src/auth.js +386 -100
- package/src/cli.js +18 -11
- package/src/commands/auth.js +208 -70
- package/src/commands/decisiontables.js +178 -0
- package/src/commands/dev/_simple.js +1 -2
- package/src/commands/dev/flows.js +92 -838
- package/src/commands/docs/db.js +60 -2
- package/src/commands/perf.js +143 -0
- package/src/decision-table-crud.js +266 -0
- package/src/decision-table-inspection.js +260 -0
- package/src/errors.js +1 -1
- package/src/flow-inspection.js +844 -0
- package/src/flow-publish.js +357 -0
- package/src/helpers.js +7 -1
- package/src/perf-capture.js +112 -0
- package/src/perf.js +335 -0
- package/src/sdk.js +120 -25
- package/src/session.js +14 -0
- package/src/commands/instance.js +0 -362
package/src/commands/instance.js
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { declareCapabilities } from '../capabilities.js';
|
|
2
|
-
|
|
3
|
-
// The report separates activity in the selected window from current totals.
|
|
4
|
-
// Missing or protected tables stay visible as unavailable instead of aborting
|
|
5
|
-
// the whole report.
|
|
6
|
-
const DATA_VOLUME = [
|
|
7
|
-
['Task records', 'task'],
|
|
8
|
-
['Incidents', 'incident'],
|
|
9
|
-
['Catalog tasks', 'sc_task'],
|
|
10
|
-
['Requested items', 'sc_req_item'],
|
|
11
|
-
['Requests', 'sc_request'],
|
|
12
|
-
['Problems', 'problem'],
|
|
13
|
-
['Change requests', 'change_request'],
|
|
14
|
-
['CMDB CI records', 'cmdb_ci'],
|
|
15
|
-
];
|
|
16
|
-
const DATA_VOLUME_CONCURRENCY = 2;
|
|
17
|
-
const DATA_VOLUME_TIMEOUT = 120000;
|
|
18
|
-
|
|
19
|
-
const CONFIGURED_AUTOMATION = [
|
|
20
|
-
['Scripted REST APIs', 'sys_ws_definition'],
|
|
21
|
-
['Scripted REST operations', 'sys_ws_operation'],
|
|
22
|
-
['Inbound email actions', 'sysevent_in_email_action'],
|
|
23
|
-
['SOAP messages', 'sys_soap_message'],
|
|
24
|
-
['Scheduled scripts', 'sysauto_script'],
|
|
25
|
-
['Async business rules', 'sys_script', 'when=async'],
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
declareCapabilities('instance', {});
|
|
29
|
-
|
|
30
|
-
function nextDate(date) {
|
|
31
|
-
const next = new Date(`${date}T00:00:00Z`);
|
|
32
|
-
if (Number.isNaN(next.getTime())) return '';
|
|
33
|
-
next.setUTCDate(next.getUTCDate() + 1);
|
|
34
|
-
return next.toISOString().slice(0, 10);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function buildActivityQuery({ query, since, until } = {}) {
|
|
38
|
-
if (query) return String(query);
|
|
39
|
-
const clauses = [];
|
|
40
|
-
if (since) clauses.push(`sys_created_on>=${since}`);
|
|
41
|
-
if (until) clauses.push(`sys_created_on<${nextDate(until) || until}`);
|
|
42
|
-
if (clauses.length) return clauses.join('^');
|
|
43
|
-
return 'sys_created_on>=javascript:gs.daysAgoStart(1)';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function addQuery(base, extra) {
|
|
47
|
-
return [base, extra].filter(Boolean).join('^');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function numeric(value) {
|
|
51
|
-
const number = Number(String(value ?? '').replaceAll(',', ''));
|
|
52
|
-
return Number.isFinite(number) ? number : null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function groupField(group, field) {
|
|
56
|
-
return group?.groupby_fields?.find(item => item?.field === field)?.value || 'unknown';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function groupCount(group) {
|
|
60
|
-
return numeric(group?.stats?.count ?? group?.count) ?? 0;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function metricValue(stats, metric, field) {
|
|
64
|
-
const value = stats?.[metric];
|
|
65
|
-
return numeric(typeof value === 'object' ? value?.[field] : value);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function safeCount(sdk, label, table, query = '', options = {}) {
|
|
69
|
-
try {
|
|
70
|
-
return { label, table, query, count: await sdk.aggregateCount(table, query, options) };
|
|
71
|
-
} catch (err) {
|
|
72
|
-
const message = err.message || 'request failed';
|
|
73
|
-
const deadline = options.timeout && message === 'Request timed out'
|
|
74
|
-
? ` after ${Math.round(options.timeout / 1000)} seconds`
|
|
75
|
-
: '';
|
|
76
|
-
return { label, table, query, count: null, unavailable: `${message}${deadline}` };
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function mapWithConcurrency(items, limit, worker) {
|
|
81
|
-
const results = new Array(items.length);
|
|
82
|
-
let nextIndex = 0;
|
|
83
|
-
const run = async () => {
|
|
84
|
-
while (nextIndex < items.length) {
|
|
85
|
-
const index = nextIndex;
|
|
86
|
-
nextIndex += 1;
|
|
87
|
-
results[index] = await worker(items[index]);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run));
|
|
91
|
-
return results;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function safeAggregate(sdk, label, table, options) {
|
|
95
|
-
try {
|
|
96
|
-
const result = await sdk.aggregate(table, options);
|
|
97
|
-
return {
|
|
98
|
-
label,
|
|
99
|
-
table,
|
|
100
|
-
groups: Array.isArray(result?.groups) ? result.groups : (Array.isArray(result?.stats) ? result.stats : []),
|
|
101
|
-
stats: result?.stats && !Array.isArray(result.stats) ? result.stats : null,
|
|
102
|
-
};
|
|
103
|
-
} catch (err) {
|
|
104
|
-
return { label, table, groups: [], stats: null, unavailable: err.message || 'request failed' };
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function safeStatsResult(result) {
|
|
109
|
-
const stats = result.stats || result.groups[0]?.stats || {};
|
|
110
|
-
return {
|
|
111
|
-
avg_ms: metricValue(stats, 'avg', 'response_time'),
|
|
112
|
-
max_ms: metricValue(stats, 'max', 'response_time'),
|
|
113
|
-
...(result.unavailable ? { unavailable: result.unavailable } : {}),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function buildDateList(since, until) {
|
|
118
|
-
if (!since || !until || !nextDate(until)) return [];
|
|
119
|
-
const dates = [];
|
|
120
|
-
let current = since;
|
|
121
|
-
for (let i = 0; i < 366 && current && current <= until; i += 1) {
|
|
122
|
-
dates.push(current);
|
|
123
|
-
current = nextDate(current);
|
|
124
|
-
}
|
|
125
|
-
return dates;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function formatCount(value) {
|
|
129
|
-
if (value == null) return 'unavailable';
|
|
130
|
-
return Number(value).toLocaleString('en-US');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function formatDuration(ms) {
|
|
134
|
-
if (ms == null) return 'unavailable';
|
|
135
|
-
if (ms >= 3600000) return `${(ms / 3600000).toFixed(1)} hours`;
|
|
136
|
-
if (ms >= 60000) return `${(ms / 60000).toFixed(1)} minutes`;
|
|
137
|
-
if (ms >= 1000) return `${(ms / 1000).toFixed(1)} seconds`;
|
|
138
|
-
return `${Math.round(ms * 100) / 100} ms`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function formatMetricRows(rows) {
|
|
142
|
-
return rows.map(row => ` ${row.label}: ${formatCount(row.count)}${row.unavailable ? ` (${row.unavailable})` : ''}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function formatFlowRows(flow) {
|
|
146
|
-
const lines = [` Flow executions: ${formatCount(flow.total_count)}`];
|
|
147
|
-
for (const row of flow.states) lines.push(` ${row.state}: ${formatCount(row.count)}`);
|
|
148
|
-
if (flow.unavailable) lines.push(` Unavailable: ${flow.unavailable}`);
|
|
149
|
-
return lines;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function formatTransactions(transactions) {
|
|
153
|
-
const lines = [` Total transactions: ${formatCount(transactions.total_count)}`];
|
|
154
|
-
if (transactions.types.length) {
|
|
155
|
-
for (const row of transactions.types) {
|
|
156
|
-
lines.push('');
|
|
157
|
-
lines.push(` ${row.type}`);
|
|
158
|
-
lines.push(` Count: ${formatCount(row.count)}`);
|
|
159
|
-
lines.push(` Average response: ${formatDuration(row.avg_response_time_ms)}`);
|
|
160
|
-
lines.push(` Maximum response: ${formatDuration(row.max_response_time_ms)}`);
|
|
161
|
-
}
|
|
162
|
-
} else {
|
|
163
|
-
lines.push(' (no transaction types)');
|
|
164
|
-
}
|
|
165
|
-
if (transactions.unavailable) lines.push(` Unavailable: ${transactions.unavailable}`);
|
|
166
|
-
return lines;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function formatDailyFlows(rows) {
|
|
170
|
-
if (!rows.length) return [];
|
|
171
|
-
return [
|
|
172
|
-
'',
|
|
173
|
-
' Daily flow executions',
|
|
174
|
-
' Date Count',
|
|
175
|
-
...rows.map(row => ` ${row.date} ${formatCount(row.count)}`),
|
|
176
|
-
];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function formatDailyImports(rows) {
|
|
180
|
-
if (!rows.length) return [];
|
|
181
|
-
return [
|
|
182
|
-
'',
|
|
183
|
-
' Daily import activity',
|
|
184
|
-
' Date Import sets Import rows',
|
|
185
|
-
...rows.map(row => ` ${row.date} ${formatCount(row.import_sets).padStart(11)} ${formatCount(row.import_rows).padStart(12)}`),
|
|
186
|
-
];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function formatInstanceGlance(data) {
|
|
190
|
-
const lines = [
|
|
191
|
-
'INSTANCE AT A GLANCE',
|
|
192
|
-
`Instance: ${data.context.instance_url}`,
|
|
193
|
-
`Activity window: ${data.window.since || 'last 24 hours'} through ${data.window.until || 'now'}`,
|
|
194
|
-
'Configuration and data totals: current',
|
|
195
|
-
'',
|
|
196
|
-
'1. INBOUND AUTOMATION',
|
|
197
|
-
...formatMetricRows(data.inbound_automation.metrics),
|
|
198
|
-
' Scripted REST separation: not separated from other REST transactions',
|
|
199
|
-
'',
|
|
200
|
-
'2. ASYNCHRONOUS AUTOMATION',
|
|
201
|
-
...formatFlowRows(data.asynchronous_automation),
|
|
202
|
-
` Async business rules configured: ${formatCount(data.asynchronous_automation.async_business_rules_configured)}`,
|
|
203
|
-
...formatDailyFlows(data.asynchronous_automation.daily_flow_executions),
|
|
204
|
-
'',
|
|
205
|
-
'3. SCHEDULED WORK',
|
|
206
|
-
` Scheduled scripts configured: ${formatCount(data.scheduled_work.scheduled_scripts_configured)}`,
|
|
207
|
-
` Scheduler transactions: ${formatCount(data.scheduled_work.scheduler_transactions)}`,
|
|
208
|
-
` Average scheduler transaction time: ${formatDuration(data.scheduled_work.scheduler_transaction_time.avg_ms)}`,
|
|
209
|
-
` Maximum scheduler transaction time: ${formatDuration(data.scheduled_work.scheduler_transaction_time.max_ms)}`,
|
|
210
|
-
` Import sets: ${formatCount(data.scheduled_work.import_sets)}`,
|
|
211
|
-
` Import rows: ${formatCount(data.scheduled_work.import_rows)}`,
|
|
212
|
-
...formatDailyImports(data.scheduled_work.daily_import_activity),
|
|
213
|
-
'',
|
|
214
|
-
'4. QUERY AND SECURITY CONTROLS',
|
|
215
|
-
...formatMetricRows(data.query_and_security_controls),
|
|
216
|
-
'',
|
|
217
|
-
'5. DATA VOLUME',
|
|
218
|
-
...formatMetricRows(data.data_volume),
|
|
219
|
-
'',
|
|
220
|
-
'6. TRANSACTION DETAIL',
|
|
221
|
-
...formatTransactions(data.transactions),
|
|
222
|
-
];
|
|
223
|
-
return `${lines.join('\n')}\n`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function summarizeTransactions(groups) {
|
|
227
|
-
return groups.map(group => {
|
|
228
|
-
const stats = group?.stats || {};
|
|
229
|
-
return {
|
|
230
|
-
type: groupField(group, 'type'),
|
|
231
|
-
count: groupCount(group),
|
|
232
|
-
avg_response_time_ms: metricValue(stats, 'avg', 'response_time'),
|
|
233
|
-
max_response_time_ms: metricValue(stats, 'max', 'response_time'),
|
|
234
|
-
};
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function stateRows(groups) {
|
|
239
|
-
return groups.map(group => ({ state: groupField(group, 'state'), count: groupCount(group) }));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function datesFor(argv) {
|
|
243
|
-
return argv.daily === false ? [] : buildDateList(argv.since, argv.until);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function instanceCmd(wrap) {
|
|
247
|
-
return {
|
|
248
|
-
command: 'instance [subcommand]',
|
|
249
|
-
aliases: ['overview'],
|
|
250
|
-
describe: 'Inspect the current ServiceNow instance',
|
|
251
|
-
builder: y => y.command({
|
|
252
|
-
command: 'glance',
|
|
253
|
-
aliases: ['at-a-glance', 'summary'],
|
|
254
|
-
describe: 'Show a readable instance health and activity report',
|
|
255
|
-
builder: yargs => yargs
|
|
256
|
-
.option('query', { type: 'string', describe: 'Encoded query for activity metrics' })
|
|
257
|
-
.option('since', { type: 'string', describe: 'Activity start date (YYYY-MM-DD)' })
|
|
258
|
-
.option('until', { type: 'string', describe: 'Activity end date (YYYY-MM-DD)' })
|
|
259
|
-
.option('daily', { type: 'boolean', default: true, describe: 'Include daily flow and import rows when dates are supplied' }),
|
|
260
|
-
handler: wrap(async (argv, app) => {
|
|
261
|
-
app.requireInstance();
|
|
262
|
-
const query = buildActivityQuery(argv);
|
|
263
|
-
const dailyDates = datesFor(argv);
|
|
264
|
-
const schedulerQuery = addQuery(query, 'urlLIKEscheduler');
|
|
265
|
-
const [
|
|
266
|
-
dataVolume,
|
|
267
|
-
configured,
|
|
268
|
-
flowTotal,
|
|
269
|
-
flowStates,
|
|
270
|
-
transactions,
|
|
271
|
-
restTransactions,
|
|
272
|
-
tableApi,
|
|
273
|
-
graphql,
|
|
274
|
-
inboundEmail,
|
|
275
|
-
soap,
|
|
276
|
-
schedulerTransactions,
|
|
277
|
-
schedulerStats,
|
|
278
|
-
asyncBusinessRules,
|
|
279
|
-
beforeQueryRules,
|
|
280
|
-
scriptedAcls,
|
|
281
|
-
readAcls,
|
|
282
|
-
writeAcls,
|
|
283
|
-
deleteAcls,
|
|
284
|
-
createAcls,
|
|
285
|
-
executeAcls,
|
|
286
|
-
importSets,
|
|
287
|
-
importRows,
|
|
288
|
-
dailyFlows,
|
|
289
|
-
dailyImportSets,
|
|
290
|
-
dailyImportRows,
|
|
291
|
-
] = await Promise.all([
|
|
292
|
-
mapWithConcurrency(DATA_VOLUME, DATA_VOLUME_CONCURRENCY, ([label, table]) => safeCount(app.sdk, label, table, '', { timeout: DATA_VOLUME_TIMEOUT })),
|
|
293
|
-
Promise.all(CONFIGURED_AUTOMATION.map(([label, table, tableQuery]) => safeCount(app.sdk, label, table, tableQuery))),
|
|
294
|
-
safeCount(app.sdk, 'flow_executions', 'sys_flow_context', query),
|
|
295
|
-
safeAggregate(app.sdk, 'flow_states', 'sys_flow_context', { query, groupBy: ['state'], count: true }),
|
|
296
|
-
safeAggregate(app.sdk, 'transactions', 'syslog_transaction', { query, groupBy: ['type'], count: true, averageFields: ['response_time'], maximumFields: ['response_time'] }),
|
|
297
|
-
safeCount(app.sdk, 'REST transactions', 'syslog_transaction', addQuery(query, 'type=rest')),
|
|
298
|
-
safeCount(app.sdk, 'Table API URL matches', 'syslog_transaction', addQuery(query, 'urlLIKE/api/now/table/')),
|
|
299
|
-
safeCount(app.sdk, 'GraphQL URL matches', 'syslog_transaction', addQuery(query, 'urlLIKEgraphql')),
|
|
300
|
-
safeCount(app.sdk, 'Inbound email records', 'sys_email', addQuery(query, 'type=received')),
|
|
301
|
-
safeCount(app.sdk, 'SOAP URL matches', 'syslog_transaction', addQuery(query, 'urlLIKEsoap')),
|
|
302
|
-
safeCount(app.sdk, 'Scheduler transactions', 'syslog_transaction', schedulerQuery),
|
|
303
|
-
safeAggregate(app.sdk, 'scheduler_transaction_time', 'syslog_transaction', { query: schedulerQuery, averageFields: ['response_time'], maximumFields: ['response_time'] }),
|
|
304
|
-
safeCount(app.sdk, 'async_business_rules_configured', 'sys_script', 'when=async'),
|
|
305
|
-
safeCount(app.sdk, 'Scripted before-query business rules', 'sys_script', 'when=before^action_query=true'),
|
|
306
|
-
safeCount(app.sdk, 'Scripted access controls', 'sys_security_acl', 'scriptISNOTEMPTY'),
|
|
307
|
-
safeCount(app.sdk, 'Scripted read ACLs', 'sys_security_acl', 'operation=read^scriptISNOTEMPTY'),
|
|
308
|
-
safeCount(app.sdk, 'Scripted write ACLs', 'sys_security_acl', 'operation=write^scriptISNOTEMPTY'),
|
|
309
|
-
safeCount(app.sdk, 'Scripted delete ACLs', 'sys_security_acl', 'operation=delete^scriptISNOTEMPTY'),
|
|
310
|
-
safeCount(app.sdk, 'Scripted create ACLs', 'sys_security_acl', 'operation=create^scriptISNOTEMPTY'),
|
|
311
|
-
safeCount(app.sdk, 'Scripted execute ACLs', 'sys_security_acl', 'operation=execute^scriptISNOTEMPTY'),
|
|
312
|
-
safeCount(app.sdk, 'import_sets', 'sys_import_set', query),
|
|
313
|
-
safeCount(app.sdk, 'import_rows', 'sys_import_set_row', query),
|
|
314
|
-
Promise.all(dailyDates.map(date => safeCount(app.sdk, 'flow_executions', 'sys_flow_context', buildActivityQuery({ since: date, until: date })))),
|
|
315
|
-
Promise.all(dailyDates.map(date => safeCount(app.sdk, 'import_sets', 'sys_import_set', buildActivityQuery({ since: date, until: date })))),
|
|
316
|
-
Promise.all(dailyDates.map(date => safeCount(app.sdk, 'import_rows', 'sys_import_set_row', buildActivityQuery({ since: date, until: date })))),
|
|
317
|
-
]);
|
|
318
|
-
|
|
319
|
-
const inboundMetrics = [restTransactions, tableApi, graphql, inboundEmail, soap];
|
|
320
|
-
const data = {
|
|
321
|
-
window: { query, since: argv.since || null, until: argv.until || null },
|
|
322
|
-
inbound_automation: {
|
|
323
|
-
metrics: inboundMetrics,
|
|
324
|
-
configured: configured.slice(0, 4),
|
|
325
|
-
},
|
|
326
|
-
asynchronous_automation: {
|
|
327
|
-
total_count: flowTotal.count,
|
|
328
|
-
states: stateRows(flowStates.groups),
|
|
329
|
-
async_business_rules_configured: asyncBusinessRules.count,
|
|
330
|
-
daily_flow_executions: dailyDates.map((date, index) => ({ date, count: dailyFlows[index].count })),
|
|
331
|
-
},
|
|
332
|
-
scheduled_work: {
|
|
333
|
-
scheduled_scripts_configured: configured.find(row => row.label === 'Scheduled scripts')?.count ?? null,
|
|
334
|
-
scheduler_transactions: schedulerTransactions.count,
|
|
335
|
-
scheduler_transaction_time: safeStatsResult(schedulerStats),
|
|
336
|
-
import_sets: importSets.count,
|
|
337
|
-
import_rows: importRows.count,
|
|
338
|
-
daily_import_activity: dailyDates.map((date, index) => ({ date, import_sets: dailyImportSets[index].count, import_rows: dailyImportRows[index].count })),
|
|
339
|
-
},
|
|
340
|
-
query_and_security_controls: [beforeQueryRules, scriptedAcls, readAcls, writeAcls, deleteAcls, createAcls, executeAcls],
|
|
341
|
-
data_volume: dataVolume,
|
|
342
|
-
transactions: {
|
|
343
|
-
total_count: transactions.groups.reduce((total, group) => total + groupCount(group), 0),
|
|
344
|
-
types: summarizeTransactions(transactions.groups),
|
|
345
|
-
...(transactions.unavailable ? { unavailable: transactions.unavailable } : {}),
|
|
346
|
-
},
|
|
347
|
-
context: { instance_url: app.getEffectiveInstance(), timezone: 'UTC' },
|
|
348
|
-
};
|
|
349
|
-
app.ok({ ...data, _formatted: formatInstanceGlance(data) }, {
|
|
350
|
-
summary: `Instance glance for ${app.getEffectiveInstance()}`,
|
|
351
|
-
});
|
|
352
|
-
}),
|
|
353
|
-
}),
|
|
354
|
-
handler: wrap(async (_argv, app) => {
|
|
355
|
-
app.ok({
|
|
356
|
-
command: 'instance',
|
|
357
|
-
subcommands: ['glance'],
|
|
358
|
-
_formatted: 'Run "jsn instance glance" for an instance report.\n',
|
|
359
|
-
}, { summary: 'Instance inspection commands' });
|
|
360
|
-
}),
|
|
361
|
-
};
|
|
362
|
-
}
|