@jacebenson/jsn 3.4.0 → 3.4.2
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/cli.js +5 -1
- package/src/commands/dev/flows.js +145 -27
- package/src/sdk.js +121 -24
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -225,7 +225,11 @@ export function buildCLI() {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
if (shouldCheckSkill) {
|
|
228
|
-
|
|
228
|
+
try {
|
|
229
|
+
checkSkill();
|
|
230
|
+
} catch {
|
|
231
|
+
// Non-fatal — don't crash the CLI if skill check fails
|
|
232
|
+
}
|
|
229
233
|
|
|
230
234
|
try {
|
|
231
235
|
fs.mkdirSync(globalConfigDir(), { recursive: true });
|
|
@@ -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
|
+
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('');
|
|
@@ -185,12 +209,14 @@ function formatFlowInspection(inspection, instanceURL) {
|
|
|
185
209
|
lines.push('');
|
|
186
210
|
lines.push('⚡ FLOW STRUCTURE');
|
|
187
211
|
lines.push('─'.repeat(50));
|
|
188
|
-
const structureLines = formatFlowStructure(inspection);
|
|
212
|
+
const structureLines = await formatFlowStructure(inspection, ctx);
|
|
189
213
|
lines.push(...structureLines);
|
|
190
|
-
// Note limitation
|
|
191
|
-
|
|
214
|
+
// Note limitation only when we couldn't reconstruct detail from the payload OR the gzip fallback
|
|
215
|
+
const hasPayload = inspection.payload && Object.keys(inspection.payload).length > 0;
|
|
216
|
+
const hasDecodedLogic = (inspection.flowLogicInstances || []).some(l => l._decodedValues);
|
|
217
|
+
if (!hasPayload && !hasDecodedLogic) {
|
|
192
218
|
lines.push('');
|
|
193
|
-
lines.push(' (step detail limited —
|
|
219
|
+
lines.push(' (step detail limited — no flow definition data available)');
|
|
194
220
|
}
|
|
195
221
|
|
|
196
222
|
lines.push('');
|
|
@@ -254,15 +280,15 @@ function inferFlowVersion(inspection) {
|
|
|
254
280
|
return 'Unset';
|
|
255
281
|
}
|
|
256
282
|
|
|
257
|
-
function formatFlowStructure(inspection) {
|
|
283
|
+
async function formatFlowStructure(inspection, ctx) {
|
|
258
284
|
const payload = inspection.payload;
|
|
259
285
|
if (Object.keys(payload).length > 0) {
|
|
260
|
-
return formatFlowStructureFromPayload(payload);
|
|
286
|
+
return formatFlowStructureFromPayload(payload, ctx);
|
|
261
287
|
}
|
|
262
288
|
return formatFlowStructureFallback(inspection);
|
|
263
289
|
}
|
|
264
290
|
|
|
265
|
-
function formatFlowStructureFromPayload(payload) {
|
|
291
|
+
async function formatFlowStructureFromPayload(payload, ctx) {
|
|
266
292
|
const childUIDs = new Set();
|
|
267
293
|
|
|
268
294
|
function markChildren(items) {
|
|
@@ -312,10 +338,10 @@ function formatFlowStructureFromPayload(payload) {
|
|
|
312
338
|
const lines = [];
|
|
313
339
|
let stepNum = 1;
|
|
314
340
|
|
|
315
|
-
function walk(steps, indent) {
|
|
341
|
+
async function walk(steps, indent) {
|
|
316
342
|
for (const step of steps) {
|
|
317
343
|
const pad = ' '.repeat(indent);
|
|
318
|
-
lines.push(...formatStepLine(stepNum, pad, step));
|
|
344
|
+
lines.push(...await formatStepLine(stepNum, pad, step, ctx));
|
|
319
345
|
stepNum++;
|
|
320
346
|
|
|
321
347
|
if (step.stepType !== 'logic') continue;
|
|
@@ -332,11 +358,11 @@ function formatFlowStructureFromPayload(payload) {
|
|
|
332
358
|
});
|
|
333
359
|
}
|
|
334
360
|
children.sort((a, b) => a.order - b.order);
|
|
335
|
-
walk(children, indent + 1);
|
|
361
|
+
await walk(children, indent + 1);
|
|
336
362
|
}
|
|
337
363
|
}
|
|
338
364
|
|
|
339
|
-
walk(roots, 0);
|
|
365
|
+
await walk(roots, 0);
|
|
340
366
|
return lines;
|
|
341
367
|
}
|
|
342
368
|
|
|
@@ -351,7 +377,7 @@ function formatFlowStructureFallback(inspection) {
|
|
|
351
377
|
getStringField(action, 'display_text'),
|
|
352
378
|
'Action',
|
|
353
379
|
);
|
|
354
|
-
steps.push({ order: parseOrderField(action), text: name });
|
|
380
|
+
steps.push({ order: parseOrderField(action), text: name, comment: getStringField(action, 'comment') });
|
|
355
381
|
}
|
|
356
382
|
}
|
|
357
383
|
|
|
@@ -363,7 +389,12 @@ function formatFlowStructureFallback(inspection) {
|
|
|
363
389
|
getStringField(logic, 'display_text'),
|
|
364
390
|
'Logic',
|
|
365
391
|
);
|
|
366
|
-
steps.push({
|
|
392
|
+
steps.push({
|
|
393
|
+
order: parseOrderField(logic),
|
|
394
|
+
text: name,
|
|
395
|
+
comment: getStringField(logic, 'comment'),
|
|
396
|
+
decoded: logic._decodedValues || null,
|
|
397
|
+
});
|
|
367
398
|
}
|
|
368
399
|
}
|
|
369
400
|
|
|
@@ -385,15 +416,65 @@ function formatFlowStructureFallback(inspection) {
|
|
|
385
416
|
return [' (no steps found)'];
|
|
386
417
|
}
|
|
387
418
|
|
|
388
|
-
|
|
419
|
+
const lines = [];
|
|
420
|
+
steps.forEach((step, i) => {
|
|
421
|
+
const num = `${i + 1}.`;
|
|
422
|
+
// Decoded gzip values carry the same input detail as a V2 payload
|
|
423
|
+
if (step.decoded && (step.text === 'If' || step.text === 'Else If')) {
|
|
424
|
+
let condition = '';
|
|
425
|
+
let conditionLabel = '';
|
|
426
|
+
if (Array.isArray(step.decoded.inputs)) {
|
|
427
|
+
for (const raw of step.decoded.inputs) {
|
|
428
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
429
|
+
const inputName = getStringField(raw, 'name');
|
|
430
|
+
if (inputName === 'condition') {
|
|
431
|
+
condition = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
432
|
+
}
|
|
433
|
+
if (inputName === 'condition_name') {
|
|
434
|
+
conditionLabel = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
let displayText = step.text;
|
|
439
|
+
if (conditionLabel) {
|
|
440
|
+
displayText = `${step.text}: ${conditionLabel}`;
|
|
441
|
+
} else if (condition && condition.length < 60) {
|
|
442
|
+
displayText = `${step.text}: ${condition}`;
|
|
443
|
+
}
|
|
444
|
+
lines.push(` ${num} ${displayText}`);
|
|
445
|
+
if (condition && condition.length >= 60 && !conditionLabel) {
|
|
446
|
+
lines.push(` Condition: ${condition}`);
|
|
447
|
+
}
|
|
448
|
+
} else if (step.decoded && step.text === 'Set Flow Variables') {
|
|
449
|
+
lines.push(` ${num} ${step.text}`);
|
|
450
|
+
const vars = step.decoded.variables || step.decoded.flowVariables;
|
|
451
|
+
if (Array.isArray(vars) && vars.length > 0) {
|
|
452
|
+
lines.push(` Variables Set:`);
|
|
453
|
+
for (const raw of vars) {
|
|
454
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
455
|
+
const varName = getStringField(raw, 'name');
|
|
456
|
+
const varValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
457
|
+
if (!varName) continue;
|
|
458
|
+
lines.push(varValue ? ` • ${varName} = ${varValue}` : ` • ${varName}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
lines.push(` ${num} ${step.text}`);
|
|
463
|
+
}
|
|
464
|
+
if (step.comment) {
|
|
465
|
+
lines.push(` Annotation: ${step.comment}`);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return lines;
|
|
389
470
|
}
|
|
390
471
|
|
|
391
|
-
function formatStepLine(stepNum, pad, step) {
|
|
472
|
+
async function formatStepLine(stepNum, pad, step, ctx) {
|
|
392
473
|
switch (step.stepType) {
|
|
393
474
|
case 'logic':
|
|
394
475
|
return formatLogicStep(stepNum, pad, step.data);
|
|
395
476
|
case 'subflow':
|
|
396
|
-
return formatSubFlowStep(stepNum, pad, step.data);
|
|
477
|
+
return formatSubFlowStep(stepNum, pad, step.data, ctx);
|
|
397
478
|
default:
|
|
398
479
|
return formatActionStep(stepNum, pad, step.data);
|
|
399
480
|
}
|
|
@@ -425,8 +506,9 @@ function formatActionStep(stepNum, pad, action) {
|
|
|
425
506
|
}
|
|
426
507
|
}
|
|
427
508
|
|
|
509
|
+
const showsTableSuffix = tableName && (actionName === 'Update Record' || actionName === 'Create or Update Record');
|
|
428
510
|
let actionDisplay = actionName;
|
|
429
|
-
if (
|
|
511
|
+
if (showsTableSuffix) {
|
|
430
512
|
actionDisplay = actionName + ' - ' + tableName;
|
|
431
513
|
}
|
|
432
514
|
|
|
@@ -441,7 +523,7 @@ function formatActionStep(stepNum, pad, action) {
|
|
|
441
523
|
for (const raw of action.inputs) {
|
|
442
524
|
if (!raw || typeof raw !== 'object') continue;
|
|
443
525
|
const inputName = getStringField(raw, 'name');
|
|
444
|
-
if (inputName === 'table_name') continue;
|
|
526
|
+
if (inputName === 'table_name' && showsTableSuffix) continue;
|
|
445
527
|
|
|
446
528
|
let inputValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
447
529
|
if (!inputValue) continue;
|
|
@@ -461,7 +543,7 @@ function formatActionStep(stepNum, pad, action) {
|
|
|
461
543
|
return lines;
|
|
462
544
|
}
|
|
463
545
|
|
|
464
|
-
function formatSubFlowStep(stepNum, pad, subFlow) {
|
|
546
|
+
export async function formatSubFlowStep(stepNum, pad, subFlow, ctx) {
|
|
465
547
|
const lines = [];
|
|
466
548
|
const subFlowName = firstNonEmpty(
|
|
467
549
|
getNestedString(subFlow, 'subFlowType', 'fName'),
|
|
@@ -479,7 +561,43 @@ function formatSubFlowStep(stepNum, pad, subFlow) {
|
|
|
479
561
|
lines.push(`${pad}${stepNum}. ↪ ${subFlowName}`);
|
|
480
562
|
}
|
|
481
563
|
|
|
482
|
-
|
|
564
|
+
// Recurse into the subflow when depth allows and we haven't already seen it.
|
|
565
|
+
// Note: `subflowSysId` on the instance is a snapshot id (sys_hub_flow_snapshot),
|
|
566
|
+
// NOT the flow id. The embedded `subFlow.parentFlow` is the real flow sys_id.
|
|
567
|
+
const subflowSysId = firstNonEmpty(
|
|
568
|
+
getNestedString(subFlow, 'subFlow', 'parentFlow'),
|
|
569
|
+
getStringField(subFlow, 'subflowSysId'),
|
|
570
|
+
getNestedString(subFlow, 'subFlow', 'sys_id'),
|
|
571
|
+
getStringField(subFlow, 'sys_id'),
|
|
572
|
+
);
|
|
573
|
+
const depth = ctx?.depth ?? 1;
|
|
574
|
+
const canRecurse = ctx?.sdk && subflowSysId && depth > 1 && !ctx.visited.has(subflowSysId);
|
|
575
|
+
|
|
576
|
+
if (!canRecurse) {
|
|
577
|
+
if (!subflowSysId) {
|
|
578
|
+
lines.push(`${pad} (subflow definition not found)`);
|
|
579
|
+
} else if (depth <= 1) {
|
|
580
|
+
lines.push(`${pad} jsn flows show "${subFlowName}"`);
|
|
581
|
+
}
|
|
582
|
+
return lines;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
ctx.visited.add(subflowSysId);
|
|
586
|
+
try {
|
|
587
|
+
const subInspection = await ctx.sdk.inspectFlow(subflowSysId);
|
|
588
|
+
const subCtx = { ...ctx, depth: depth - 1 };
|
|
589
|
+
const subLines = await formatFlowStructure(subInspection, subCtx);
|
|
590
|
+
if (subLines.length === 0) {
|
|
591
|
+
lines.push(`${pad} (no steps found)`);
|
|
592
|
+
} else {
|
|
593
|
+
const innerPad = pad + ' ';
|
|
594
|
+
for (const l of subLines) {
|
|
595
|
+
lines.push(innerPad + l.replace(/^ {2}/, ''));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} catch (e) {
|
|
599
|
+
lines.push(`${pad} (could not load subflow: ${e.message})`);
|
|
600
|
+
}
|
|
483
601
|
return lines;
|
|
484
602
|
}
|
|
485
603
|
|
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
|
|
|
@@ -246,28 +247,51 @@ export class SDKClient {
|
|
|
246
247
|
flowOutputs: [],
|
|
247
248
|
};
|
|
248
249
|
|
|
249
|
-
// 2)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
250
|
+
// 2) Prefer the ProcessFlow API — the Flow Designer UI's own source.
|
|
251
|
+
// Returns complete structure for both V2 and legacy V1 flows (the
|
|
252
|
+
// version payload field is empty for V1 flows).
|
|
253
|
+
let payloadLoaded = false;
|
|
254
|
+
try {
|
|
255
|
+
const pfResponse = await this.request(`${this.baseURL}/api/now/processflow/flow/${flowSysID}`, { method: 'GET' });
|
|
256
|
+
const pfData = pfResponse?.result?.data;
|
|
257
|
+
if (pfData && typeof pfData === 'object' && !pfResponse?.result?.errorMessage) {
|
|
258
|
+
inspection.payload = pfData;
|
|
259
|
+
if (!inspection.flow.version) {
|
|
260
|
+
const pfVersion = getStringField(pfData, 'version');
|
|
261
|
+
if (pfVersion) inspection.flow.version = pfVersion;
|
|
262
|
+
}
|
|
263
|
+
hydrateFlowBlocks(inspection.payload);
|
|
264
|
+
extractPayloadData(inspection, inspection.payload);
|
|
265
|
+
payloadLoaded = true;
|
|
261
266
|
}
|
|
267
|
+
} catch {
|
|
268
|
+
// ProcessFlow API may not exist on older instances — fall through
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 2b) Fallback: latest flow version payload
|
|
272
|
+
if (!payloadLoaded) {
|
|
273
|
+
const versionQuery = new URLSearchParams();
|
|
274
|
+
versionQuery.set('sysparm_display_value', 'all');
|
|
275
|
+
versionQuery.set('sysparm_limit', '1');
|
|
276
|
+
versionQuery.set('sysparm_query', `flow=${flowSysID}^ORDERBYDESCsys_updated_on`);
|
|
277
|
+
versionQuery.set('sysparm_fields', 'sys_id,flow,version,payload,sys_updated_on');
|
|
278
|
+
|
|
279
|
+
const versionRecords = await this.list('sys_hub_flow_version', versionQuery);
|
|
280
|
+
if (versionRecords && versionRecords.length > 0) {
|
|
281
|
+
inspection.version = versionRecords[0];
|
|
282
|
+
if (!inspection.flow.version) {
|
|
283
|
+
inspection.flow.version = getStringField(versionRecords[0], 'version');
|
|
284
|
+
}
|
|
262
285
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
const payload = getStringField(versionRecords[0], 'payload');
|
|
287
|
+
if (payload) {
|
|
288
|
+
try {
|
|
289
|
+
const payloadData = JSON.parse(payload);
|
|
290
|
+
inspection.payload = payloadData;
|
|
291
|
+
extractPayloadData(inspection, payloadData);
|
|
292
|
+
} catch {
|
|
293
|
+
// ignore parse error
|
|
294
|
+
}
|
|
271
295
|
}
|
|
272
296
|
}
|
|
273
297
|
}
|
|
@@ -295,15 +319,24 @@ export class SDKClient {
|
|
|
295
319
|
}
|
|
296
320
|
|
|
297
321
|
if (!inspection.flowLogicInstances || inspection.flowLogicInstances.length === 0) {
|
|
298
|
-
const logicTables = [
|
|
299
|
-
|
|
322
|
+
const logicTables = [
|
|
323
|
+
{ table: 'sys_hub_flow_logic', payloadField: 'inputs' },
|
|
324
|
+
{ table: 'sys_hub_flow_logic_instance_v2', payloadField: 'values' },
|
|
325
|
+
];
|
|
326
|
+
for (const { table, payloadField } of logicTables) {
|
|
300
327
|
const logicQuery = new URLSearchParams();
|
|
301
328
|
logicQuery.set('sysparm_display_value', 'all');
|
|
302
329
|
logicQuery.set('sysparm_query', `flow=${flowSysID}^ORDERBYorder`);
|
|
303
|
-
logicQuery.set('sysparm_fields',
|
|
330
|
+
logicQuery.set('sysparm_fields', `sys_id,order,name,display_text,comment,parent_ui_id,logic_definition,${payloadField}`);
|
|
304
331
|
logicQuery.set('sysparm_limit', '200');
|
|
305
332
|
const records = await this.list(table, logicQuery);
|
|
306
|
-
if (records)
|
|
333
|
+
if (records) {
|
|
334
|
+
for (const rec of records) {
|
|
335
|
+
const decoded = decodeGzipJson(rec[payloadField]);
|
|
336
|
+
if (decoded) rec._decodedValues = decoded;
|
|
337
|
+
}
|
|
338
|
+
inspection.flowLogicInstances.push(...records);
|
|
339
|
+
}
|
|
307
340
|
}
|
|
308
341
|
inspection.flowLogicInstances.sort((a, b) => parseOrderField(a) - parseOrderField(b));
|
|
309
342
|
}
|
|
@@ -634,8 +667,72 @@ function toMapSlice(v) {
|
|
|
634
667
|
return v.filter(item => item && typeof item === 'object');
|
|
635
668
|
}
|
|
636
669
|
|
|
670
|
+
/**
|
|
671
|
+
* The ProcessFlow API returns flow structure as flat arrays where children
|
|
672
|
+
* reference their parent logic step via a `parent` / `parent_ui_id` field
|
|
673
|
+
* (a uiUniqueIdentifier value). The payload-based renderer expects nested
|
|
674
|
+
* `flowBlock` arrays on each logic instance. Rebuild that nesting here so
|
|
675
|
+
* both data sources render identically.
|
|
676
|
+
*/
|
|
677
|
+
export function hydrateFlowBlocks(payload) {
|
|
678
|
+
const blockKeys = ['actionInstances', 'flowLogicInstances', 'subFlowInstances'];
|
|
679
|
+
const items = [];
|
|
680
|
+
for (const key of blockKeys) {
|
|
681
|
+
if (Array.isArray(payload[key])) {
|
|
682
|
+
for (const item of payload[key]) {
|
|
683
|
+
if (item && typeof item === 'object') items.push(item);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (items.length === 0) return;
|
|
688
|
+
|
|
689
|
+
// If the payload already has nested flowBlock arrays (version-payload
|
|
690
|
+
// shape), leave it alone.
|
|
691
|
+
const logicItems = Array.isArray(payload.flowLogicInstances) ? payload.flowLogicInstances : [];
|
|
692
|
+
if (logicItems.some(l => l && typeof l === 'object' && Array.isArray(l.flowBlock))) return;
|
|
693
|
+
|
|
694
|
+
// Build parent → children map keyed by uiUniqueIdentifier
|
|
695
|
+
const childrenByParent = new Map();
|
|
696
|
+
for (const item of items) {
|
|
697
|
+
const parentId = getStringField(item, 'parent_ui_id') || getStringField(item, 'parent');
|
|
698
|
+
if (!parentId) continue;
|
|
699
|
+
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);
|
|
700
|
+
childrenByParent.get(parentId).push(item);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Attach sorted flowBlock to each logic instance
|
|
704
|
+
for (const logic of logicItems) {
|
|
705
|
+
if (!logic || typeof logic !== 'object') continue;
|
|
706
|
+
const uid = getStringField(logic, 'uiUniqueIdentifier');
|
|
707
|
+
const children = uid ? childrenByParent.get(uid) : null;
|
|
708
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
709
|
+
children.sort((a, b) => parseOrderField(a) - parseOrderField(b));
|
|
710
|
+
logic.flowBlock = children;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
637
715
|
function parseOrderField(record) {
|
|
638
716
|
const order = getStringField(record, 'order');
|
|
639
717
|
const n = parseInt(order, 10);
|
|
640
718
|
return isNaN(n) ? 0 : n;
|
|
641
719
|
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Decode a ServiceNow flow-logic "values" field: base64-gzip JSON.
|
|
723
|
+
* Returns the parsed object, or null when the field is empty / not gzip / unparseable.
|
|
724
|
+
*/
|
|
725
|
+
export function decodeGzipJson(value) {
|
|
726
|
+
if (!value) return null;
|
|
727
|
+
const s = (typeof value === 'object' && value !== null)
|
|
728
|
+
? (value.value ?? value.display_value ?? '')
|
|
729
|
+
: String(value);
|
|
730
|
+
const trimmed = s.trim();
|
|
731
|
+
if (!trimmed.startsWith('H4sI')) return null; // not gzip base64
|
|
732
|
+
try {
|
|
733
|
+
const decoded = gunzipSync(Buffer.from(trimmed, 'base64')).toString('utf-8');
|
|
734
|
+
return JSON.parse(decoded);
|
|
735
|
+
} catch {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|