@jacebenson/jsn 4.2.2 → 4.2.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/README.md +20 -0
- package/package.json +1 -1
- package/src/auth.js +12 -7
- package/src/cli.js +6 -0
- package/src/commands/auth.js +36 -14
- package/src/commands/dev/flows.js +120 -2
- package/src/commands/dev/rest.js +8 -1
- package/src/commands/instance.js +362 -0
- package/src/commands/platform.js +127 -0
- package/src/commands/records.js +46 -1
- package/src/commands/transactions.js +100 -0
- package/src/flow-context.js +184 -0
- package/src/records/inspect.js +14 -13
- package/src/sdk.js +68 -6
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { getStringField } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
export const FLOW_CONTEXT_CANDIDATE_FIELDS = [
|
|
4
|
+
'name', 'flow', 'flow_catalog_model', 'execution_id', 'state', 'status',
|
|
5
|
+
'started', 'start_time', 'started_at', 'ended', 'end_time', 'ended_at', 'completed_on',
|
|
6
|
+
'sys_created_on', 'sys_updated_on', 'duration', 'run_time',
|
|
7
|
+
'execution_duration', 'execution_state', 'wait_for', 'waiting_for', 'error', 'error_message',
|
|
8
|
+
'exception', 'message', 'source_record', 'engine_major_version', 'origins',
|
|
9
|
+
'calling_source',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function findValue(record, names) {
|
|
13
|
+
for (const name of names) {
|
|
14
|
+
const result = getStringField(record, name).trim();
|
|
15
|
+
if (result) return { value: result, field: name };
|
|
16
|
+
}
|
|
17
|
+
return { value: '', field: null };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dateValue(input) {
|
|
21
|
+
if (!input) return null;
|
|
22
|
+
const date = new Date(String(input).replace(' ', 'T') + (String(input).includes('Z') ? '' : 'Z'));
|
|
23
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function secondsFromDuration(input) {
|
|
27
|
+
if (!input) return null;
|
|
28
|
+
if (/^\d+(\.\d+)?$/.test(input)) return Number(input);
|
|
29
|
+
const parts = input.split(':').map(Number);
|
|
30
|
+
if (parts.some(Number.isNaN) || parts.length > 3) return null;
|
|
31
|
+
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
32
|
+
if (parts.length === 2) return parts[0] * 60 + parts[1];
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isWaiting(status) {
|
|
37
|
+
return /wait|queue|pause|pending/i.test(status);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRunning(status) {
|
|
41
|
+
return /running|executing|processing/i.test(status);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeFlowContext(record, { now = new Date() } = {}) {
|
|
45
|
+
const status = findValue(record, ['state', 'status', 'execution_state']);
|
|
46
|
+
const runtimeStarted = findValue(record, ['started', 'start_time', 'started_at']);
|
|
47
|
+
const started = runtimeStarted.value ? runtimeStarted : findValue(record, ['sys_created_on']);
|
|
48
|
+
const ended = findValue(record, ['ended', 'end_time', 'ended_at', 'completed_on']);
|
|
49
|
+
const duration = findValue(record, ['duration', 'run_time', 'execution_duration']);
|
|
50
|
+
const waitFor = findValue(record, ['wait_for', 'waiting_for']);
|
|
51
|
+
const error = findValue(record, ['error', 'error_message', 'exception', 'message']);
|
|
52
|
+
const startedDate = dateValue(started.value);
|
|
53
|
+
const runtimeStartedDate = dateValue(runtimeStarted.value);
|
|
54
|
+
const endedDate = dateValue(ended.value);
|
|
55
|
+
const explicitDuration = secondsFromDuration(duration.value);
|
|
56
|
+
const durationSeconds = explicitDuration ?? (runtimeStartedDate && endedDate ? Math.max(0, (endedDate - runtimeStartedDate) / 1000) : null);
|
|
57
|
+
const ageSeconds = startedDate ? Math.max(0, (new Date(now) - startedDate) / 1000) : null;
|
|
58
|
+
const runtimeAgeSeconds = runtimeStartedDate ? Math.max(0, (new Date(now) - runtimeStartedDate) / 1000) : null;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
sys_id: findValue(record, ['sys_id']).value,
|
|
62
|
+
flow: findValue(record, ['name', 'flow', 'flow_catalog_model']).value || '(deleted flow)',
|
|
63
|
+
execution_id: findValue(record, ['execution_id']).value,
|
|
64
|
+
status: status.value,
|
|
65
|
+
started: started.value || null,
|
|
66
|
+
ended: ended.value || null,
|
|
67
|
+
duration_seconds: durationSeconds == null ? null : Math.round(durationSeconds),
|
|
68
|
+
waiting_age_seconds: isWaiting(status.value) && ageSeconds != null ? Math.round(ageSeconds) : null,
|
|
69
|
+
execution_age_seconds: isRunning(status.value) && runtimeAgeSeconds != null ? Math.round(runtimeAgeSeconds) : null,
|
|
70
|
+
wait_for: waitFor.value || null,
|
|
71
|
+
error: error.value || null,
|
|
72
|
+
version: findValue(record, ['engine_major_version']).value || null,
|
|
73
|
+
source_record: findValue(record, ['source_record']).value || null,
|
|
74
|
+
field_mapping: {
|
|
75
|
+
status: status.field,
|
|
76
|
+
started: started.field,
|
|
77
|
+
ended: ended.field,
|
|
78
|
+
duration: duration.field || (durationSeconds != null ? 'derived' : null),
|
|
79
|
+
wait_for: waitFor.field,
|
|
80
|
+
error: error.field,
|
|
81
|
+
},
|
|
82
|
+
missing_fields: ['started', 'ended', 'duration'].filter(field => {
|
|
83
|
+
const mapping = { started: started.field, ended: ended.field, duration: duration.field || (durationSeconds != null ? 'derived' : null) };
|
|
84
|
+
return !mapping[field];
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatFlowContextSummary(summary) {
|
|
90
|
+
const lines = ['', '▶ FLOW EXECUTIONS', '─'.repeat(72), ` Total matching: ${summary.total_count ?? '(unavailable)'}`, ` Sampled: ${summary.sample_count}`, ''];
|
|
91
|
+
for (const [flow, metrics] of Object.entries(summary.by_flow).sort(([a], [b]) => a.localeCompare(b))) {
|
|
92
|
+
const statuses = Object.entries(metrics.statuses).map(([status, count]) => `${status}: ${count}`).join(', ');
|
|
93
|
+
const duration = metrics.duration_seconds.count > 0
|
|
94
|
+
? `${metrics.duration_seconds.min}s-${metrics.duration_seconds.max}s, avg ${metrics.duration_seconds.average}s`
|
|
95
|
+
: '(no completed durations)';
|
|
96
|
+
lines.push(` ${flow} (${metrics.count}${metrics.sampled ? `, ${metrics.sampled} sampled` : ''})`);
|
|
97
|
+
lines.push(` Status: ${statuses}`);
|
|
98
|
+
lines.push(` Duration sample: ${duration}`);
|
|
99
|
+
if (metrics.waiting_count > 0) lines.push(` Waiting sample: ${metrics.waiting_count}`);
|
|
100
|
+
}
|
|
101
|
+
return lines.join('\n') + '\n';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function aggregateFlowContextMappings(executions) {
|
|
105
|
+
const keys = ['status', 'started', 'ended', 'duration', 'wait_for', 'error'];
|
|
106
|
+
return Object.fromEntries(keys.map(key => {
|
|
107
|
+
const values = [...new Set(executions.map(execution => execution.field_mapping?.[key]).filter(Boolean))];
|
|
108
|
+
return [key, values.length === 1 ? values[0] : values.length > 1 ? 'mixed' : null];
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function mergeFlowContextStats(groups, sampledSummary, totalCount) {
|
|
113
|
+
const byFlow = {};
|
|
114
|
+
for (const group of groups || []) {
|
|
115
|
+
const fields = Object.fromEntries((group.groupby_fields || []).map(field => [field.field, field.value]));
|
|
116
|
+
const flow = fields.name || '(deleted flow)';
|
|
117
|
+
const status = fields.state || fields.status || '(unknown)';
|
|
118
|
+
const bucket = byFlow[flow] ||= { count: 0, sampled: 0, statuses: {}, waiting_count: 0, duration_seconds: { count: 0, min: null, max: null, average: null } };
|
|
119
|
+
const count = Number(group.stats?.count ?? 0);
|
|
120
|
+
bucket.count += Number.isFinite(count) ? count : 0;
|
|
121
|
+
bucket.statuses[status] = (bucket.statuses[status] || 0) + (Number.isFinite(count) ? count : 0);
|
|
122
|
+
}
|
|
123
|
+
for (const [flow, sample] of Object.entries(sampledSummary.by_flow)) {
|
|
124
|
+
const bucket = byFlow[flow] ||= { count: 0, sampled: 0, statuses: {}, waiting_count: 0, duration_seconds: { count: 0, min: null, max: null, average: null } };
|
|
125
|
+
bucket.sampled = sample.count;
|
|
126
|
+
if (bucket.count === 0) bucket.count = sample.count;
|
|
127
|
+
bucket.waiting_count = sample.waiting_count;
|
|
128
|
+
bucket.duration_seconds = sample.duration_seconds;
|
|
129
|
+
}
|
|
130
|
+
return { total_count: totalCount, sample_count: sampledSummary.count, by_flow: byFlow };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildFlowContextQuery({ record, since, until, query } = {}) {
|
|
134
|
+
const parts = [];
|
|
135
|
+
for (const [label, value, operator] of [['record', record, '='], ['since', since, '>='], ['until', until, '<=']]) {
|
|
136
|
+
if (value == null || value === '') continue;
|
|
137
|
+
if (/[\\^<>=]/.test(value)) throw new Error(`invalid --${label}: use a timestamp or sys_id without query operators`);
|
|
138
|
+
parts.push(`${label === 'record' ? 'source_record' : 'sys_created_on'}${operator}${value}`);
|
|
139
|
+
}
|
|
140
|
+
if (query) parts.push(query);
|
|
141
|
+
parts.push('ORDERBYDESCsys_created_on');
|
|
142
|
+
return parts.join('^');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function summarizeFlowContexts(executions) {
|
|
146
|
+
const byFlow = {};
|
|
147
|
+
for (const execution of executions) {
|
|
148
|
+
const flow = execution.flow || '(deleted flow)';
|
|
149
|
+
const bucket = byFlow[flow] ||= { count: 0, statuses: {}, waiting_count: 0, duration_seconds: { count: 0, min: null, max: null, average: null, _sum: 0 } };
|
|
150
|
+
bucket.count += 1;
|
|
151
|
+
const status = execution.status || '(unknown)';
|
|
152
|
+
bucket.statuses[status] = (bucket.statuses[status] || 0) + 1;
|
|
153
|
+
if (execution.waiting_age_seconds != null) bucket.waiting_count += 1;
|
|
154
|
+
if (execution.duration_seconds != null) {
|
|
155
|
+
const stats = bucket.duration_seconds;
|
|
156
|
+
stats.count += 1;
|
|
157
|
+
stats.min = stats.min == null ? execution.duration_seconds : Math.min(stats.min, execution.duration_seconds);
|
|
158
|
+
stats.max = stats.max == null ? execution.duration_seconds : Math.max(stats.max, execution.duration_seconds);
|
|
159
|
+
stats._sum += execution.duration_seconds;
|
|
160
|
+
stats.average = Math.round(stats._sum / stats.count);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const bucket of Object.values(byFlow)) delete bucket.duration_seconds._sum;
|
|
164
|
+
return { count: executions.length, by_flow: byFlow };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function discoverFlowContextFields(sdk) {
|
|
168
|
+
const fallback = new Set(['sys_id', ...FLOW_CONTEXT_CANDIDATE_FIELDS]);
|
|
169
|
+
try {
|
|
170
|
+
const params = new URLSearchParams();
|
|
171
|
+
params.set('sysparm_query', `name=sys_flow_context^elementIN${FLOW_CONTEXT_CANDIDATE_FIELDS.join(',')}`);
|
|
172
|
+
params.set('sysparm_fields', 'element');
|
|
173
|
+
params.set('sysparm_limit', '100');
|
|
174
|
+
const dictionary = await sdk.list('sys_dictionary', params);
|
|
175
|
+
const discovered = new Set(['sys_id']);
|
|
176
|
+
for (const row of dictionary) {
|
|
177
|
+
const element = getStringField(row, 'element');
|
|
178
|
+
if (element) discovered.add(element);
|
|
179
|
+
}
|
|
180
|
+
return discovered.size > 1 ? discovered : fallback;
|
|
181
|
+
} catch {
|
|
182
|
+
return fallback;
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/records/inspect.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isHexString, getStringField, assertSafeExactMatch } from '../helpers.js';
|
|
2
|
+
import { discoverFlowContextFields, normalizeFlowContext } from '../flow-context.js';
|
|
2
3
|
|
|
3
4
|
export async function resolveIdentifier(app, table, identifier) {
|
|
4
5
|
// If it looks like a sys_id (32 hex chars), use it directly
|
|
@@ -164,20 +165,15 @@ async function fetchBusinessRules(app, table) {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
async function fetchFlows(app, sysId) {
|
|
167
|
-
const params = new URLSearchParams();
|
|
168
|
-
params.set('sysparm_query', `source_record=${sysId}`);
|
|
169
|
-
params.set('sysparm_limit', '20');
|
|
170
|
-
params.set('sysparm_fields', 'flow_catalog_model,name,execution_id,state,engine_major_version,sys_created_on,origins,calling_source');
|
|
171
|
-
params.set('sysparm_display_value', 'all');
|
|
172
168
|
try {
|
|
169
|
+
const fields = await discoverFlowContextFields(app.sdk);
|
|
170
|
+
const params = new URLSearchParams();
|
|
171
|
+
params.set('sysparm_query', `source_record=${sysId}^ORDERBYDESCsys_created_on`);
|
|
172
|
+
params.set('sysparm_limit', '20');
|
|
173
|
+
params.set('sysparm_fields', [...fields].join(','));
|
|
174
|
+
params.set('sysparm_display_value', 'all');
|
|
173
175
|
const records = await app.sdk.list('sys_flow_context', params);
|
|
174
|
-
return records.map(r => (
|
|
175
|
-
flow: r.name?.display_value || r.name || r.flow_catalog_model?.display_value || r.flow_catalog_model?.value || '(deleted flow)',
|
|
176
|
-
executionId: r.execution_id?.display_value || r.execution_id,
|
|
177
|
-
state: r.state?.display_value || r.state,
|
|
178
|
-
version: r.engine_major_version?.display_value || r.engine_major_version,
|
|
179
|
-
started: r.sys_created_on?.display_value || r.sys_created_on,
|
|
180
|
-
}));
|
|
176
|
+
return records.map(r => normalizeFlowContext(r));
|
|
181
177
|
} catch {
|
|
182
178
|
return []; // Flow Designer might not be installed
|
|
183
179
|
}
|
|
@@ -245,8 +241,13 @@ export function formatInspectOutput(data) {
|
|
|
245
241
|
} else {
|
|
246
242
|
for (const f of data.flows) {
|
|
247
243
|
lines.push(` Flow: ${f.flow}`);
|
|
248
|
-
lines.push(` Status: ${f.
|
|
244
|
+
lines.push(` Status: ${f.status || '(unknown)'}${f.execution_id ? ` | Execution: ${f.execution_id}` : ''}`);
|
|
249
245
|
if (f.started) lines.push(` Started: ${f.started}`);
|
|
246
|
+
if (f.ended) lines.push(` Ended: ${f.ended}`);
|
|
247
|
+
if (f.duration_seconds != null) lines.push(` Duration: ${f.duration_seconds}s`);
|
|
248
|
+
if (f.waiting_age_seconds != null) lines.push(` Waiting age: ${f.waiting_age_seconds}s`);
|
|
249
|
+
if (f.wait_for) lines.push(` Waiting for: ${f.wait_for}`);
|
|
250
|
+
if (f.error) lines.push(` Error: ${f.error}`);
|
|
250
251
|
lines.push('');
|
|
251
252
|
}
|
|
252
253
|
}
|
package/src/sdk.js
CHANGED
|
@@ -59,16 +59,17 @@ export class SDKClient {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
async request(endpoint, opts = {}) {
|
|
62
|
+
const { timeout = this.timeout, ...requestOptions } = opts;
|
|
62
63
|
const controller = new AbortController();
|
|
63
|
-
const timer = setTimeout(() => controller.abort(),
|
|
64
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
64
65
|
|
|
65
66
|
try {
|
|
66
67
|
const req = new Request(endpoint, {
|
|
67
|
-
...
|
|
68
|
+
...requestOptions,
|
|
68
69
|
signal: controller.signal,
|
|
69
70
|
});
|
|
70
71
|
req.headers.set('Accept', 'application/json');
|
|
71
|
-
if (
|
|
72
|
+
if (requestOptions.body && typeof requestOptions.body === 'string') {
|
|
72
73
|
req.headers.set('Content-Type', 'application/json');
|
|
73
74
|
}
|
|
74
75
|
if (this.domain) {
|
|
@@ -460,12 +461,49 @@ export class SDKClient {
|
|
|
460
461
|
return inspection;
|
|
461
462
|
}
|
|
462
463
|
|
|
463
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Resolve a custom action and return its internal step instances.
|
|
466
|
+
* The action reference on sys_hub_step_instance points at the shared
|
|
467
|
+
* Action Type Base, so the action definition sys_id is the query value.
|
|
468
|
+
*
|
|
469
|
+
* @param {string} identifier - action name or sys_id
|
|
470
|
+
* @returns {Promise<{action: object, steps: object[]}|null>}
|
|
471
|
+
*/
|
|
472
|
+
async inspectCustomAction(identifier) {
|
|
473
|
+
if (!identifier) return null;
|
|
474
|
+
const isSysID = /^[0-9a-fA-F]{32}$/.test(String(identifier));
|
|
475
|
+
const actionQuery = new URLSearchParams();
|
|
476
|
+
actionQuery.set('sysparm_display_value', 'all');
|
|
477
|
+
actionQuery.set('sysparm_limit', '1');
|
|
478
|
+
actionQuery.set('sysparm_query', isSysID ? `sys_id=${identifier}` : `name=${identifier}`);
|
|
479
|
+
actionQuery.set('sysparm_fields', 'sys_id,name,master_snapshot,latest_snapshot,state,active');
|
|
480
|
+
|
|
481
|
+
const actions = await this.list('sys_hub_action_type_definition', actionQuery);
|
|
482
|
+
if (!actions || actions.length === 0) return null;
|
|
483
|
+
|
|
484
|
+
const action = actions[0];
|
|
485
|
+
const actionSysID = getStringField(action, 'sys_id');
|
|
486
|
+
if (!actionSysID) return null;
|
|
487
|
+
|
|
488
|
+
const stepQuery = new URLSearchParams();
|
|
489
|
+
stepQuery.set('sysparm_display_value', 'all');
|
|
490
|
+
stepQuery.set('sysparm_query', `action=${actionSysID}^ORDERBYorder`);
|
|
491
|
+
stepQuery.set('sysparm_fields', 'sys_id,label,action,step_type,order,section,inputs,outputs,extended_inputs,extended_outputs,error_handling_type');
|
|
492
|
+
stepQuery.set('sysparm_limit', '200');
|
|
493
|
+
const steps = await this.list('sys_hub_step_instance', stepQuery);
|
|
494
|
+
|
|
495
|
+
return { action, steps: steps || [] };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async aggregateCount(table, queryStr, options = {}) {
|
|
464
499
|
const params = new URLSearchParams();
|
|
465
500
|
params.set('sysparm_count', 'true');
|
|
466
501
|
if (queryStr) params.set('sysparm_query', queryStr);
|
|
467
502
|
const endpoint = `${this.baseURL}/api/now/stats/${table}?${params.toString()}`;
|
|
468
|
-
const result = await this.request(endpoint, {
|
|
503
|
+
const result = await this.request(endpoint, {
|
|
504
|
+
method: 'GET',
|
|
505
|
+
...(options.timeout ? { timeout: options.timeout } : {}),
|
|
506
|
+
});
|
|
469
507
|
const stats = result?.result?.stats;
|
|
470
508
|
if (!stats) return 0;
|
|
471
509
|
|
|
@@ -497,7 +535,31 @@ export class SDKClient {
|
|
|
497
535
|
return 0;
|
|
498
536
|
}
|
|
499
537
|
|
|
500
|
-
|
|
538
|
+
async aggregate(table, options = {}) {
|
|
539
|
+
const params = new URLSearchParams();
|
|
540
|
+
const fields = (value) => Array.isArray(value) ? value.filter(Boolean).join(',') : String(value || '');
|
|
541
|
+
const groupBy = fields(options.groupBy);
|
|
542
|
+
const orderBy = fields(options.orderBy);
|
|
543
|
+
|
|
544
|
+
if (options.query) params.set('sysparm_query', options.query);
|
|
545
|
+
if (options.count !== false) params.set('sysparm_count', 'true');
|
|
546
|
+
if (groupBy) params.set('sysparm_group_by', groupBy);
|
|
547
|
+
if (fields(options.averageFields)) params.set('sysparm_avg_fields', fields(options.averageFields));
|
|
548
|
+
if (fields(options.sumFields)) params.set('sysparm_sum_fields', fields(options.sumFields));
|
|
549
|
+
if (fields(options.minimumFields)) params.set('sysparm_min_fields', fields(options.minimumFields));
|
|
550
|
+
if (fields(options.maximumFields)) params.set('sysparm_max_fields', fields(options.maximumFields));
|
|
551
|
+
if (orderBy) params.set('sysparm_order_by', orderBy);
|
|
552
|
+
|
|
553
|
+
const endpoint = `${this.baseURL}/api/now/stats/${table}?${params.toString()}`;
|
|
554
|
+
const result = await this.request(endpoint, { method: 'GET' });
|
|
555
|
+
const payload = result?.result || {};
|
|
556
|
+
const numericKeys = Object.keys(payload).filter(key => /^\d+$/.test(key));
|
|
557
|
+
if (numericKeys.length > 0 && numericKeys.length === Object.keys(payload).length) {
|
|
558
|
+
return { groups: numericKeys.sort((a, b) => Number(a) - Number(b)).map(key => payload[key]) };
|
|
559
|
+
}
|
|
560
|
+
return payload;
|
|
561
|
+
}
|
|
562
|
+
|
|
501
563
|
|
|
502
564
|
/**
|
|
503
565
|
* Export an update set to XML via the fluent export endpoint.
|