@jacebenson/jsn 3.4.1 → 3.4.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/commands/dev/flows.js +177 -32
- package/src/sdk.js +139 -24
package/package.json
CHANGED
|
@@ -14,7 +14,12 @@ export function flowsCmd(wrap) {
|
|
|
14
14
|
builder: (y) => y
|
|
15
15
|
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
16
16
|
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
|
|
17
|
-
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' })
|
|
17
|
+
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' })
|
|
18
|
+
.option('depth', {
|
|
19
|
+
type: 'number',
|
|
20
|
+
default: 2,
|
|
21
|
+
describe: 'Expand nested subflows to this depth (1 = subflow names only, 2 = one level, 3+ = deeper)',
|
|
22
|
+
}),
|
|
18
23
|
handler: wrap(async (argv, app) => {
|
|
19
24
|
app.requireInstance();
|
|
20
25
|
const columns = argv.columns ? argv.columns.split(',') : ['name', 'active', 'description', 'sys_created_by', 'sys_updated_on'];
|
|
@@ -28,7 +33,13 @@ export function flowsCmd(wrap) {
|
|
|
28
33
|
if (picked === undefined) return; // user cancelled
|
|
29
34
|
if (picked) {
|
|
30
35
|
const inspection = await app.sdk.inspectFlow(getStringField(picked, 'sys_id'));
|
|
31
|
-
const
|
|
36
|
+
const ctx = {
|
|
37
|
+
sdk: app.sdk,
|
|
38
|
+
instanceURL: app.getEffectiveInstance(),
|
|
39
|
+
depth: Math.max(1, Math.floor(argv.depth ?? 2)),
|
|
40
|
+
visited: new Set([inspection.flow.sysID]),
|
|
41
|
+
};
|
|
42
|
+
const formatted = await formatFlowInspection(inspection, ctx);
|
|
32
43
|
return app.ok({ ...inspection, _formatted: formatted }, {
|
|
33
44
|
summary: `Flow: ${inspection.flow.name}`,
|
|
34
45
|
breadcrumbs: [{ action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' }],
|
|
@@ -56,9 +67,21 @@ export function flowsCmd(wrap) {
|
|
|
56
67
|
command: 'show <identifier>',
|
|
57
68
|
aliases: ['get'],
|
|
58
69
|
describe: 'Show flow details by name or sys_id',
|
|
70
|
+
builder: (y) => y
|
|
71
|
+
.option('depth', {
|
|
72
|
+
type: 'number',
|
|
73
|
+
default: 2,
|
|
74
|
+
describe: 'Expand nested subflows to this depth (1 = subflow names only, 2 = one level, 3+ = deeper)',
|
|
75
|
+
}),
|
|
59
76
|
handler: wrap(async (argv, app) => {
|
|
60
77
|
const inspection = await app.sdk.inspectFlow(argv.identifier);
|
|
61
|
-
const
|
|
78
|
+
const ctx = {
|
|
79
|
+
sdk: app.sdk,
|
|
80
|
+
instanceURL: app.getEffectiveInstance(),
|
|
81
|
+
depth: Math.max(1, Math.floor(argv.depth ?? 2)),
|
|
82
|
+
visited: new Set([inspection.flow.sysID]),
|
|
83
|
+
};
|
|
84
|
+
const formatted = await formatFlowInspection(inspection, ctx);
|
|
62
85
|
const data = { ...inspection, _formatted: formatted };
|
|
63
86
|
app.ok(data, {
|
|
64
87
|
summary: `Flow: ${inspection.flow.name}`,
|
|
@@ -109,15 +132,16 @@ export function flowsCmd(wrap) {
|
|
|
109
132
|
console.log('');
|
|
110
133
|
console.log('Run "jsn flows <command> --help" for details.');
|
|
111
134
|
console.log('');
|
|
112
|
-
console.log('Note: Flow
|
|
113
|
-
console.log('
|
|
114
|
-
console.log('
|
|
135
|
+
console.log('Note: Flow structure comes from the ProcessFlow API (the Flow');
|
|
136
|
+
console.log('Designer UI source), so V1, V2, and subflow definitions all');
|
|
137
|
+
console.log('render with full action inputs and conditions.');
|
|
115
138
|
},
|
|
116
139
|
};
|
|
117
140
|
}
|
|
118
141
|
|
|
119
|
-
function formatFlowInspection(inspection,
|
|
142
|
+
export async function formatFlowInspection(inspection, ctx) {
|
|
120
143
|
const lines = [];
|
|
144
|
+
const instanceURL = ctx?.instanceURL || '';
|
|
121
145
|
const flow = inspection.flow;
|
|
122
146
|
|
|
123
147
|
lines.push('');
|
|
@@ -132,6 +156,27 @@ function formatFlowInspection(inspection, instanceURL) {
|
|
|
132
156
|
lines.push(` Link: ${instanceURL}/sys_hub_flow.do?sys_id=${flow.sysID}`);
|
|
133
157
|
}
|
|
134
158
|
|
|
159
|
+
// Flow variables section
|
|
160
|
+
if (inspection.flowVariables && inspection.flowVariables.length > 0) {
|
|
161
|
+
const vars = [...inspection.flowVariables].sort((a, b) => {
|
|
162
|
+
const ao = parseInt(getStringField(a, 'order'), 10) || 0;
|
|
163
|
+
const bo = parseInt(getStringField(b, 'order'), 10) || 0;
|
|
164
|
+
return ao - bo;
|
|
165
|
+
});
|
|
166
|
+
lines.push('');
|
|
167
|
+
lines.push('▶ FLOW VARIABLES');
|
|
168
|
+
lines.push('─'.repeat(50));
|
|
169
|
+
for (const v of vars) {
|
|
170
|
+
const name = firstNonEmpty(getStringField(v, 'label'), getStringField(v, 'name'), 'Variable');
|
|
171
|
+
const typeName = firstNonEmpty(getStringField(v, 'type_label'), getStringField(v, 'type'), '');
|
|
172
|
+
if (typeName) {
|
|
173
|
+
lines.push(` • ${name}: ${typeName}`);
|
|
174
|
+
} else {
|
|
175
|
+
lines.push(` • ${name}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
135
180
|
// Subflow I/O section
|
|
136
181
|
if (flow.type && flow.type.toLowerCase() === 'subflow') {
|
|
137
182
|
lines.push('');
|
|
@@ -185,12 +230,14 @@ function formatFlowInspection(inspection, instanceURL) {
|
|
|
185
230
|
lines.push('');
|
|
186
231
|
lines.push('⚡ FLOW STRUCTURE');
|
|
187
232
|
lines.push('─'.repeat(50));
|
|
188
|
-
const structureLines = formatFlowStructure(inspection);
|
|
233
|
+
const structureLines = await formatFlowStructure(inspection, ctx);
|
|
189
234
|
lines.push(...structureLines);
|
|
190
|
-
// Note limitation
|
|
191
|
-
|
|
235
|
+
// Note limitation only when we couldn't reconstruct detail from the payload OR the gzip fallback
|
|
236
|
+
const hasPayload = inspection.payload && Object.keys(inspection.payload).length > 0;
|
|
237
|
+
const hasDecodedLogic = (inspection.flowLogicInstances || []).some(l => l._decodedValues);
|
|
238
|
+
if (!hasPayload && !hasDecodedLogic) {
|
|
192
239
|
lines.push('');
|
|
193
|
-
lines.push(' (step detail limited —
|
|
240
|
+
lines.push(' (step detail limited — no flow definition data available)');
|
|
194
241
|
}
|
|
195
242
|
|
|
196
243
|
lines.push('');
|
|
@@ -254,15 +301,15 @@ function inferFlowVersion(inspection) {
|
|
|
254
301
|
return 'Unset';
|
|
255
302
|
}
|
|
256
303
|
|
|
257
|
-
function formatFlowStructure(inspection) {
|
|
304
|
+
async function formatFlowStructure(inspection, ctx) {
|
|
258
305
|
const payload = inspection.payload;
|
|
259
306
|
if (Object.keys(payload).length > 0) {
|
|
260
|
-
return formatFlowStructureFromPayload(payload);
|
|
307
|
+
return formatFlowStructureFromPayload(payload, ctx);
|
|
261
308
|
}
|
|
262
309
|
return formatFlowStructureFallback(inspection);
|
|
263
310
|
}
|
|
264
311
|
|
|
265
|
-
function formatFlowStructureFromPayload(payload) {
|
|
312
|
+
async function formatFlowStructureFromPayload(payload, ctx) {
|
|
266
313
|
const childUIDs = new Set();
|
|
267
314
|
|
|
268
315
|
function markChildren(items) {
|
|
@@ -312,10 +359,10 @@ function formatFlowStructureFromPayload(payload) {
|
|
|
312
359
|
const lines = [];
|
|
313
360
|
let stepNum = 1;
|
|
314
361
|
|
|
315
|
-
function walk(steps, indent) {
|
|
362
|
+
async function walk(steps, indent) {
|
|
316
363
|
for (const step of steps) {
|
|
317
364
|
const pad = ' '.repeat(indent);
|
|
318
|
-
lines.push(...formatStepLine(stepNum, pad, step));
|
|
365
|
+
lines.push(...await formatStepLine(stepNum, pad, step, ctx));
|
|
319
366
|
stepNum++;
|
|
320
367
|
|
|
321
368
|
if (step.stepType !== 'logic') continue;
|
|
@@ -332,11 +379,11 @@ function formatFlowStructureFromPayload(payload) {
|
|
|
332
379
|
});
|
|
333
380
|
}
|
|
334
381
|
children.sort((a, b) => a.order - b.order);
|
|
335
|
-
walk(children, indent + 1);
|
|
382
|
+
await walk(children, indent + 1);
|
|
336
383
|
}
|
|
337
384
|
}
|
|
338
385
|
|
|
339
|
-
walk(roots, 0);
|
|
386
|
+
await walk(roots, 0);
|
|
340
387
|
return lines;
|
|
341
388
|
}
|
|
342
389
|
|
|
@@ -351,7 +398,7 @@ function formatFlowStructureFallback(inspection) {
|
|
|
351
398
|
getStringField(action, 'display_text'),
|
|
352
399
|
'Action',
|
|
353
400
|
);
|
|
354
|
-
steps.push({ order: parseOrderField(action), text: name });
|
|
401
|
+
steps.push({ order: parseOrderField(action), text: name, comment: getStringField(action, 'comment') });
|
|
355
402
|
}
|
|
356
403
|
}
|
|
357
404
|
|
|
@@ -363,7 +410,12 @@ function formatFlowStructureFallback(inspection) {
|
|
|
363
410
|
getStringField(logic, 'display_text'),
|
|
364
411
|
'Logic',
|
|
365
412
|
);
|
|
366
|
-
steps.push({
|
|
413
|
+
steps.push({
|
|
414
|
+
order: parseOrderField(logic),
|
|
415
|
+
text: name,
|
|
416
|
+
comment: getStringField(logic, 'comment'),
|
|
417
|
+
decoded: logic._decodedValues || null,
|
|
418
|
+
});
|
|
367
419
|
}
|
|
368
420
|
}
|
|
369
421
|
|
|
@@ -385,21 +437,71 @@ function formatFlowStructureFallback(inspection) {
|
|
|
385
437
|
return [' (no steps found)'];
|
|
386
438
|
}
|
|
387
439
|
|
|
388
|
-
|
|
440
|
+
const lines = [];
|
|
441
|
+
steps.forEach((step, i) => {
|
|
442
|
+
const num = `${i + 1}.`;
|
|
443
|
+
// Decoded gzip values carry the same input detail as a V2 payload
|
|
444
|
+
if (step.decoded && (step.text === 'If' || step.text === 'Else If')) {
|
|
445
|
+
let condition = '';
|
|
446
|
+
let conditionLabel = '';
|
|
447
|
+
if (Array.isArray(step.decoded.inputs)) {
|
|
448
|
+
for (const raw of step.decoded.inputs) {
|
|
449
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
450
|
+
const inputName = getStringField(raw, 'name');
|
|
451
|
+
if (inputName === 'condition') {
|
|
452
|
+
condition = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
453
|
+
}
|
|
454
|
+
if (inputName === 'condition_name') {
|
|
455
|
+
conditionLabel = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
let displayText = step.text;
|
|
460
|
+
if (conditionLabel) {
|
|
461
|
+
displayText = `${step.text}: ${conditionLabel}`;
|
|
462
|
+
} else if (condition && condition.length < 60) {
|
|
463
|
+
displayText = `${step.text}: ${condition}`;
|
|
464
|
+
}
|
|
465
|
+
lines.push(` ${num} ${displayText}`);
|
|
466
|
+
if (condition && condition.length >= 60 && !conditionLabel) {
|
|
467
|
+
lines.push(` Condition: ${condition}`);
|
|
468
|
+
}
|
|
469
|
+
} else if (step.decoded && step.text === 'Set Flow Variables') {
|
|
470
|
+
lines.push(` ${num} ${step.text}`);
|
|
471
|
+
const vars = step.decoded.variables || step.decoded.flowVariables;
|
|
472
|
+
if (Array.isArray(vars) && vars.length > 0) {
|
|
473
|
+
lines.push(` Variables Set:`);
|
|
474
|
+
for (const raw of vars) {
|
|
475
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
476
|
+
const varName = getStringField(raw, 'name');
|
|
477
|
+
const varValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
478
|
+
if (!varName) continue;
|
|
479
|
+
lines.push(varValue ? ` • ${varName} = ${varValue}` : ` • ${varName}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
lines.push(` ${num} ${step.text}`);
|
|
484
|
+
}
|
|
485
|
+
if (step.comment) {
|
|
486
|
+
lines.push(` Annotation: ${step.comment}`);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return lines;
|
|
389
491
|
}
|
|
390
492
|
|
|
391
|
-
function formatStepLine(stepNum, pad, step) {
|
|
493
|
+
async function formatStepLine(stepNum, pad, step, ctx) {
|
|
392
494
|
switch (step.stepType) {
|
|
393
495
|
case 'logic':
|
|
394
496
|
return formatLogicStep(stepNum, pad, step.data);
|
|
395
497
|
case 'subflow':
|
|
396
|
-
return formatSubFlowStep(stepNum, pad, step.data);
|
|
498
|
+
return formatSubFlowStep(stepNum, pad, step.data, ctx);
|
|
397
499
|
default:
|
|
398
500
|
return formatActionStep(stepNum, pad, step.data);
|
|
399
501
|
}
|
|
400
502
|
}
|
|
401
503
|
|
|
402
|
-
function formatActionStep(stepNum, pad, action) {
|
|
504
|
+
export function formatActionStep(stepNum, pad, action) {
|
|
403
505
|
const lines = [];
|
|
404
506
|
let actionName = firstNonEmpty(
|
|
405
507
|
getNestedString(action, 'actionType', 'fName'),
|
|
@@ -425,8 +527,9 @@ function formatActionStep(stepNum, pad, action) {
|
|
|
425
527
|
}
|
|
426
528
|
}
|
|
427
529
|
|
|
530
|
+
const showsTableSuffix = tableName && (actionName === 'Update Record' || actionName === 'Create or Update Record');
|
|
428
531
|
let actionDisplay = actionName;
|
|
429
|
-
if (
|
|
532
|
+
if (showsTableSuffix) {
|
|
430
533
|
actionDisplay = actionName + ' - ' + tableName;
|
|
431
534
|
}
|
|
432
535
|
|
|
@@ -441,27 +544,33 @@ function formatActionStep(stepNum, pad, action) {
|
|
|
441
544
|
for (const raw of action.inputs) {
|
|
442
545
|
if (!raw || typeof raw !== 'object') continue;
|
|
443
546
|
const inputName = getStringField(raw, 'name');
|
|
444
|
-
if (inputName === 'table_name') continue;
|
|
547
|
+
if (inputName === 'table_name' && showsTableSuffix) continue;
|
|
445
548
|
|
|
446
549
|
let inputValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
447
550
|
if (!inputValue) continue;
|
|
448
|
-
if (inputValue.length > 50) {
|
|
449
|
-
inputValue = inputValue.slice(0, 47) + '...';
|
|
450
|
-
}
|
|
451
551
|
|
|
452
552
|
let label = inputName;
|
|
453
553
|
if (raw.parameter && typeof raw.parameter === 'object') {
|
|
454
554
|
label = firstNonEmpty(getStringField(raw.parameter, 'label'), label);
|
|
455
555
|
}
|
|
456
556
|
|
|
457
|
-
|
|
557
|
+
// ServiceNow writes multi-field payloads (encoded-query style) as
|
|
558
|
+
// "field=value^field2=value2". Split those onto one line per field;
|
|
559
|
+
// plain long strings stay on a single line, untruncated.
|
|
560
|
+
if (inputValue.includes('^')) {
|
|
561
|
+
for (const field of inputValue.split('^')) {
|
|
562
|
+
if (field) lines.push(`${pad} ${label}: ${field}`);
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
lines.push(`${pad} ${label}: ${inputValue}`);
|
|
566
|
+
}
|
|
458
567
|
}
|
|
459
568
|
}
|
|
460
569
|
|
|
461
570
|
return lines;
|
|
462
571
|
}
|
|
463
572
|
|
|
464
|
-
function formatSubFlowStep(stepNum, pad, subFlow) {
|
|
573
|
+
export async function formatSubFlowStep(stepNum, pad, subFlow, ctx) {
|
|
465
574
|
const lines = [];
|
|
466
575
|
const subFlowName = firstNonEmpty(
|
|
467
576
|
getNestedString(subFlow, 'subFlowType', 'fName'),
|
|
@@ -479,7 +588,43 @@ function formatSubFlowStep(stepNum, pad, subFlow) {
|
|
|
479
588
|
lines.push(`${pad}${stepNum}. ↪ ${subFlowName}`);
|
|
480
589
|
}
|
|
481
590
|
|
|
482
|
-
|
|
591
|
+
// Recurse into the subflow when depth allows and we haven't already seen it.
|
|
592
|
+
// Note: `subflowSysId` on the instance is a snapshot id (sys_hub_flow_snapshot),
|
|
593
|
+
// NOT the flow id. The embedded `subFlow.parentFlow` is the real flow sys_id.
|
|
594
|
+
const subflowSysId = firstNonEmpty(
|
|
595
|
+
getNestedString(subFlow, 'subFlow', 'parentFlow'),
|
|
596
|
+
getStringField(subFlow, 'subflowSysId'),
|
|
597
|
+
getNestedString(subFlow, 'subFlow', 'sys_id'),
|
|
598
|
+
getStringField(subFlow, 'sys_id'),
|
|
599
|
+
);
|
|
600
|
+
const depth = ctx?.depth ?? 1;
|
|
601
|
+
const canRecurse = ctx?.sdk && subflowSysId && depth > 1 && !ctx.visited.has(subflowSysId);
|
|
602
|
+
|
|
603
|
+
if (!canRecurse) {
|
|
604
|
+
if (!subflowSysId) {
|
|
605
|
+
lines.push(`${pad} (subflow definition not found)`);
|
|
606
|
+
} else if (depth <= 1) {
|
|
607
|
+
lines.push(`${pad} jsn flows show "${subFlowName}"`);
|
|
608
|
+
}
|
|
609
|
+
return lines;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
ctx.visited.add(subflowSysId);
|
|
613
|
+
try {
|
|
614
|
+
const subInspection = await ctx.sdk.inspectFlow(subflowSysId);
|
|
615
|
+
const subCtx = { ...ctx, depth: depth - 1 };
|
|
616
|
+
const subLines = await formatFlowStructure(subInspection, subCtx);
|
|
617
|
+
if (subLines.length === 0) {
|
|
618
|
+
lines.push(`${pad} (no steps found)`);
|
|
619
|
+
} else {
|
|
620
|
+
const innerPad = pad + ' ';
|
|
621
|
+
for (const l of subLines) {
|
|
622
|
+
lines.push(innerPad + l.replace(/^ {2}/, ''));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} catch (e) {
|
|
626
|
+
lines.push(`${pad} (could not load subflow: ${e.message})`);
|
|
627
|
+
}
|
|
483
628
|
return lines;
|
|
484
629
|
}
|
|
485
630
|
|
package/src/sdk.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// ServiceNow REST API client
|
|
2
2
|
|
|
3
|
+
import { gunzipSync } from 'node:zlib';
|
|
3
4
|
import { errAuth, errAPI, errNetwork } from './errors.js';
|
|
4
5
|
import { getStringField } from './helpers.js';
|
|
5
6
|
|
|
@@ -244,30 +245,54 @@ export class SDKClient {
|
|
|
244
245
|
subFlowInstances: [],
|
|
245
246
|
flowInputs: [],
|
|
246
247
|
flowOutputs: [],
|
|
248
|
+
flowVariables: [],
|
|
247
249
|
};
|
|
248
250
|
|
|
249
|
-
// 2)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
251
|
+
// 2) Prefer the ProcessFlow API — the Flow Designer UI's own source.
|
|
252
|
+
// Returns complete structure for both V2 and legacy V1 flows (the
|
|
253
|
+
// version payload field is empty for V1 flows).
|
|
254
|
+
let payloadLoaded = false;
|
|
255
|
+
try {
|
|
256
|
+
const pfResponse = await this.request(`${this.baseURL}/api/now/processflow/flow/${flowSysID}`, { method: 'GET' });
|
|
257
|
+
const pfData = pfResponse?.result?.data;
|
|
258
|
+
if (pfData && typeof pfData === 'object' && !pfResponse?.result?.errorMessage) {
|
|
259
|
+
inspection.payload = pfData;
|
|
260
|
+
if (!inspection.flow.version) {
|
|
261
|
+
const pfVersion = getStringField(pfData, 'version');
|
|
262
|
+
if (pfVersion) inspection.flow.version = pfVersion;
|
|
263
|
+
}
|
|
264
|
+
hydrateFlowBlocks(inspection.payload);
|
|
265
|
+
extractPayloadData(inspection, inspection.payload);
|
|
266
|
+
payloadLoaded = true;
|
|
261
267
|
}
|
|
268
|
+
} catch {
|
|
269
|
+
// ProcessFlow API may not exist on older instances — fall through
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 2b) Fallback: latest flow version payload
|
|
273
|
+
if (!payloadLoaded) {
|
|
274
|
+
const versionQuery = new URLSearchParams();
|
|
275
|
+
versionQuery.set('sysparm_display_value', 'all');
|
|
276
|
+
versionQuery.set('sysparm_limit', '1');
|
|
277
|
+
versionQuery.set('sysparm_query', `flow=${flowSysID}^ORDERBYDESCsys_updated_on`);
|
|
278
|
+
versionQuery.set('sysparm_fields', 'sys_id,flow,version,payload,sys_updated_on');
|
|
279
|
+
|
|
280
|
+
const versionRecords = await this.list('sys_hub_flow_version', versionQuery);
|
|
281
|
+
if (versionRecords && versionRecords.length > 0) {
|
|
282
|
+
inspection.version = versionRecords[0];
|
|
283
|
+
if (!inspection.flow.version) {
|
|
284
|
+
inspection.flow.version = getStringField(versionRecords[0], 'version');
|
|
285
|
+
}
|
|
262
286
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
287
|
+
const payload = getStringField(versionRecords[0], 'payload');
|
|
288
|
+
if (payload) {
|
|
289
|
+
try {
|
|
290
|
+
const payloadData = JSON.parse(payload);
|
|
291
|
+
inspection.payload = payloadData;
|
|
292
|
+
extractPayloadData(inspection, payloadData);
|
|
293
|
+
} catch {
|
|
294
|
+
// ignore parse error
|
|
295
|
+
}
|
|
271
296
|
}
|
|
272
297
|
}
|
|
273
298
|
}
|
|
@@ -295,15 +320,24 @@ export class SDKClient {
|
|
|
295
320
|
}
|
|
296
321
|
|
|
297
322
|
if (!inspection.flowLogicInstances || inspection.flowLogicInstances.length === 0) {
|
|
298
|
-
const logicTables = [
|
|
299
|
-
|
|
323
|
+
const logicTables = [
|
|
324
|
+
{ table: 'sys_hub_flow_logic', payloadField: 'inputs' },
|
|
325
|
+
{ table: 'sys_hub_flow_logic_instance_v2', payloadField: 'values' },
|
|
326
|
+
];
|
|
327
|
+
for (const { table, payloadField } of logicTables) {
|
|
300
328
|
const logicQuery = new URLSearchParams();
|
|
301
329
|
logicQuery.set('sysparm_display_value', 'all');
|
|
302
330
|
logicQuery.set('sysparm_query', `flow=${flowSysID}^ORDERBYorder`);
|
|
303
|
-
logicQuery.set('sysparm_fields',
|
|
331
|
+
logicQuery.set('sysparm_fields', `sys_id,order,name,display_text,comment,parent_ui_id,logic_definition,${payloadField}`);
|
|
304
332
|
logicQuery.set('sysparm_limit', '200');
|
|
305
333
|
const records = await this.list(table, logicQuery);
|
|
306
|
-
if (records)
|
|
334
|
+
if (records) {
|
|
335
|
+
for (const rec of records) {
|
|
336
|
+
const decoded = decodeGzipJson(rec[payloadField]);
|
|
337
|
+
if (decoded) rec._decodedValues = decoded;
|
|
338
|
+
}
|
|
339
|
+
inspection.flowLogicInstances.push(...records);
|
|
340
|
+
}
|
|
307
341
|
}
|
|
308
342
|
inspection.flowLogicInstances.sort((a, b) => parseOrderField(a) - parseOrderField(b));
|
|
309
343
|
}
|
|
@@ -339,6 +373,20 @@ export class SDKClient {
|
|
|
339
373
|
if (records) inspection.flowOutputs = records;
|
|
340
374
|
}
|
|
341
375
|
|
|
376
|
+
// Flow variables fallback (payload.flowVariables may be empty on legacy
|
|
377
|
+
// payload shapes, but sys_hub_flow_variable rows exist for every flow).
|
|
378
|
+
// Note: the table only carries name/label/order — type is empty there, so
|
|
379
|
+
// prefer payload.flowVariables when available.
|
|
380
|
+
if (!inspection.flowVariables || inspection.flowVariables.length === 0) {
|
|
381
|
+
const varsQuery = new URLSearchParams();
|
|
382
|
+
varsQuery.set('sysparm_display_value', 'all');
|
|
383
|
+
varsQuery.set('sysparm_query', `model=${flowSysID}^ORDERBYorder`);
|
|
384
|
+
varsQuery.set('sysparm_fields', 'sys_id,name,label,type,order');
|
|
385
|
+
varsQuery.set('sysparm_limit', '200');
|
|
386
|
+
const records = await this.list('sys_hub_flow_variable', varsQuery);
|
|
387
|
+
if (records) inspection.flowVariables = records;
|
|
388
|
+
}
|
|
389
|
+
|
|
342
390
|
return inspection;
|
|
343
391
|
}
|
|
344
392
|
|
|
@@ -602,6 +650,9 @@ function extractPayloadData(inspection, payload) {
|
|
|
602
650
|
const outputs = toMapSlice(payload.outputs);
|
|
603
651
|
if (outputs) inspection.flowOutputs = outputs;
|
|
604
652
|
|
|
653
|
+
const flowVariables = toMapSlice(payload.flowVariables);
|
|
654
|
+
if (flowVariables) inspection.flowVariables = flowVariables;
|
|
655
|
+
|
|
605
656
|
const triggerInstances = payload.triggerInstances;
|
|
606
657
|
if (!Array.isArray(triggerInstances) || triggerInstances.length === 0) return;
|
|
607
658
|
|
|
@@ -634,8 +685,72 @@ function toMapSlice(v) {
|
|
|
634
685
|
return v.filter(item => item && typeof item === 'object');
|
|
635
686
|
}
|
|
636
687
|
|
|
688
|
+
/**
|
|
689
|
+
* The ProcessFlow API returns flow structure as flat arrays where children
|
|
690
|
+
* reference their parent logic step via a `parent` / `parent_ui_id` field
|
|
691
|
+
* (a uiUniqueIdentifier value). The payload-based renderer expects nested
|
|
692
|
+
* `flowBlock` arrays on each logic instance. Rebuild that nesting here so
|
|
693
|
+
* both data sources render identically.
|
|
694
|
+
*/
|
|
695
|
+
export function hydrateFlowBlocks(payload) {
|
|
696
|
+
const blockKeys = ['actionInstances', 'flowLogicInstances', 'subFlowInstances'];
|
|
697
|
+
const items = [];
|
|
698
|
+
for (const key of blockKeys) {
|
|
699
|
+
if (Array.isArray(payload[key])) {
|
|
700
|
+
for (const item of payload[key]) {
|
|
701
|
+
if (item && typeof item === 'object') items.push(item);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (items.length === 0) return;
|
|
706
|
+
|
|
707
|
+
// If the payload already has nested flowBlock arrays (version-payload
|
|
708
|
+
// shape), leave it alone.
|
|
709
|
+
const logicItems = Array.isArray(payload.flowLogicInstances) ? payload.flowLogicInstances : [];
|
|
710
|
+
if (logicItems.some(l => l && typeof l === 'object' && Array.isArray(l.flowBlock))) return;
|
|
711
|
+
|
|
712
|
+
// Build parent → children map keyed by uiUniqueIdentifier
|
|
713
|
+
const childrenByParent = new Map();
|
|
714
|
+
for (const item of items) {
|
|
715
|
+
const parentId = getStringField(item, 'parent_ui_id') || getStringField(item, 'parent');
|
|
716
|
+
if (!parentId) continue;
|
|
717
|
+
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);
|
|
718
|
+
childrenByParent.get(parentId).push(item);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Attach sorted flowBlock to each logic instance
|
|
722
|
+
for (const logic of logicItems) {
|
|
723
|
+
if (!logic || typeof logic !== 'object') continue;
|
|
724
|
+
const uid = getStringField(logic, 'uiUniqueIdentifier');
|
|
725
|
+
const children = uid ? childrenByParent.get(uid) : null;
|
|
726
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
727
|
+
children.sort((a, b) => parseOrderField(a) - parseOrderField(b));
|
|
728
|
+
logic.flowBlock = children;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
637
733
|
function parseOrderField(record) {
|
|
638
734
|
const order = getStringField(record, 'order');
|
|
639
735
|
const n = parseInt(order, 10);
|
|
640
736
|
return isNaN(n) ? 0 : n;
|
|
641
737
|
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Decode a ServiceNow flow-logic "values" field: base64-gzip JSON.
|
|
741
|
+
* Returns the parsed object, or null when the field is empty / not gzip / unparseable.
|
|
742
|
+
*/
|
|
743
|
+
export function decodeGzipJson(value) {
|
|
744
|
+
if (!value) return null;
|
|
745
|
+
const s = (typeof value === 'object' && value !== null)
|
|
746
|
+
? (value.value ?? value.display_value ?? '')
|
|
747
|
+
: String(value);
|
|
748
|
+
const trimmed = s.trim();
|
|
749
|
+
if (!trimmed.startsWith('H4sI')) return null; // not gzip base64
|
|
750
|
+
try {
|
|
751
|
+
const decoded = gunzipSync(Buffer.from(trimmed, 'base64')).toString('utf-8');
|
|
752
|
+
return JSON.parse(decoded);
|
|
753
|
+
} catch {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
}
|