@jacebenson/jsn 4.2.4 → 4.3.0
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 +65 -18
- package/src/session.js +14 -0
- package/src/commands/instance.js +0 -362
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
2
|
import { discoverFlowContextFields, normalizeFlowContext, summarizeFlowContexts, formatFlowContextSummary, aggregateFlowContextMappings, mergeFlowContextStats, buildFlowContextQuery } from '../../flow-context.js';
|
|
3
3
|
import { declareCapabilities } from '../../capabilities.js';
|
|
4
|
+
import { inspectFlow } from '../../flow-inspection.js';
|
|
5
|
+
import { publishFlows, publishDoctor, flowStatus } from '../../flow-publish.js';
|
|
6
|
+
import { resolveRecord, unwrapSysId } from '../../resolve-record.js';
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
function flowInspectionAdapter(app) {
|
|
9
|
+
return {
|
|
10
|
+
inspectFlow: app.sdk.inspectFlow.bind(app.sdk),
|
|
11
|
+
inspectCustomAction: app.sdk.inspectCustomAction.bind(app.sdk),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'delete', 'publish'], devAlias: true });
|
|
16
|
+
|
|
17
|
+
function renderChecks(checks) {
|
|
18
|
+
return checks.map((c) => {
|
|
19
|
+
const mark = c.skipped ? '➖' : (c.ok ? '✅' : '❌');
|
|
20
|
+
return ` ${mark} ${c.name}: ${c.detail}`;
|
|
21
|
+
}).join('\n');
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
export function flowsCmd(wrap) {
|
|
8
25
|
return {
|
|
@@ -33,18 +50,17 @@ export function flowsCmd(wrap) {
|
|
|
33
50
|
const picked = await interactiveList({
|
|
34
51
|
app, table: 'sys_hub_flow', singular: 'flow', columns, limit: argv.limit, query, labelField: 'name',
|
|
35
52
|
formatLabel: r => `${getStringField(r, 'active') === 'true' ? '🟢' : '🔴'} ${getStringField(r, 'name')}`,
|
|
53
|
+
promptFn: app.promptFn,
|
|
36
54
|
});
|
|
37
55
|
if (picked === undefined) return; // user cancelled
|
|
38
56
|
if (picked) {
|
|
39
|
-
const inspection = await
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
const inspection = await inspectFlow({
|
|
58
|
+
adapter: flowInspectionAdapter(app),
|
|
59
|
+
identifier: getStringField(picked, 'sys_id'),
|
|
42
60
|
instanceURL: app.getEffectiveInstance(),
|
|
43
|
-
depth:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const formatted = await formatFlowInspection(inspection, ctx);
|
|
47
|
-
return app.ok({ ...inspection, _formatted: formatted }, {
|
|
61
|
+
depth: argv.depth,
|
|
62
|
+
});
|
|
63
|
+
return app.ok(inspection, {
|
|
48
64
|
summary: `Flow: ${inspection.flow.name}`,
|
|
49
65
|
breadcrumbs: [{ action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' }],
|
|
50
66
|
});
|
|
@@ -134,16 +150,13 @@ export function flowsCmd(wrap) {
|
|
|
134
150
|
describe: 'Expand nested subflows to this depth (1 = subflow names only, 2 = one level, 3+ = deeper)',
|
|
135
151
|
}),
|
|
136
152
|
handler: wrap(async (argv, app) => {
|
|
137
|
-
const inspection = await
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
const inspection = await inspectFlow({
|
|
154
|
+
adapter: flowInspectionAdapter(app),
|
|
155
|
+
identifier: argv.identifier,
|
|
140
156
|
instanceURL: app.getEffectiveInstance(),
|
|
141
|
-
depth:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const formatted = await formatFlowInspection(inspection, ctx);
|
|
145
|
-
const data = { ...inspection, _formatted: formatted };
|
|
146
|
-
app.ok(data, {
|
|
157
|
+
depth: argv.depth,
|
|
158
|
+
});
|
|
159
|
+
app.ok(inspection, {
|
|
147
160
|
summary: `Flow: ${inspection.flow.name}`,
|
|
148
161
|
breadcrumbs: [
|
|
149
162
|
{ action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' },
|
|
@@ -151,6 +164,64 @@ export function flowsCmd(wrap) {
|
|
|
151
164
|
});
|
|
152
165
|
}),
|
|
153
166
|
})
|
|
167
|
+
.command({
|
|
168
|
+
command: 'publish <identifier>',
|
|
169
|
+
aliases: ['activate'],
|
|
170
|
+
describe: 'Publish (activate) a flow so it can actually run',
|
|
171
|
+
builder: (y) => y
|
|
172
|
+
.option('action', { type: 'boolean', default: false, describe: 'Treat the identifier as a custom action rather than a flow' }),
|
|
173
|
+
handler: wrap(async (argv, app) => {
|
|
174
|
+
app.requireInstance();
|
|
175
|
+
const sysID = unwrapSysId(await resolveRecord(app.sdk, {
|
|
176
|
+
table: argv.action ? 'sys_hub_action_type_definition' : 'sys_hub_flow',
|
|
177
|
+
identifier: argv.identifier,
|
|
178
|
+
matchField: 'name',
|
|
179
|
+
resource: argv.action ? 'Custom action' : 'Flow',
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
const result = argv.action
|
|
183
|
+
? await publishFlows(app.sdk, [], [sysID])
|
|
184
|
+
: await publishFlows(app.sdk, [sysID], []);
|
|
185
|
+
|
|
186
|
+
const { summary, results, via } = result;
|
|
187
|
+
const lines = results.map(r => ` ${r.status === 'success' ? '✅' : '❌'} ${r.flow_name ?? r.sys_id}: ${r.message ?? r.status}${r.errorMessage ? ` — ${r.errorMessage}` : ''}`);
|
|
188
|
+
|
|
189
|
+
return app.ok({ summary, results, via }, {
|
|
190
|
+
summary: `Published ${summary.succeeded}/${summary.total} (via ${via})\n${lines.join('\n')}`,
|
|
191
|
+
breadcrumbs: [{ action: 'status', cmd: `jsn flows status ${sysID}`, description: 'Verify the flow will run' }],
|
|
192
|
+
});
|
|
193
|
+
}),
|
|
194
|
+
})
|
|
195
|
+
.command({
|
|
196
|
+
command: 'status <identifier>',
|
|
197
|
+
describe: 'Check whether a flow is actually able to run',
|
|
198
|
+
handler: wrap(async (argv, app) => {
|
|
199
|
+
app.requireInstance();
|
|
200
|
+
const sysID = unwrapSysId(await resolveRecord(app.sdk, {
|
|
201
|
+
table: 'sys_hub_flow', identifier: argv.identifier, matchField: 'name', resource: 'Flow',
|
|
202
|
+
}));
|
|
203
|
+
const status = await flowStatus(app.sdk, sysID);
|
|
204
|
+
|
|
205
|
+
return app.ok(status, {
|
|
206
|
+
summary: `${status.ok ? '✅' : '❌'} ${status.flow.name} (${status.flow.type})\n${renderChecks(status.checks)}`,
|
|
207
|
+
breadcrumbs: status.ok ? [] : [{ action: 'publish', cmd: `jsn flows publish ${sysID}`, description: 'Publish this flow' }],
|
|
208
|
+
});
|
|
209
|
+
}),
|
|
210
|
+
})
|
|
211
|
+
.command({
|
|
212
|
+
command: 'doctor',
|
|
213
|
+
describe: 'Check whether this instance can publish flows at all',
|
|
214
|
+
handler: wrap(async (_argv, app) => {
|
|
215
|
+
app.requireInstance();
|
|
216
|
+
const report = await publishDoctor(app.sdk);
|
|
217
|
+
const hint = report.ok
|
|
218
|
+
? ''
|
|
219
|
+
: '\n\nFlow publishing is unavailable. Deploys may report success while leaving flows inert.';
|
|
220
|
+
return app.ok(report, {
|
|
221
|
+
summary: `${report.ok ? '✅' : '❌'} Flow publishing\n${renderChecks(report.checks)}${hint}`,
|
|
222
|
+
});
|
|
223
|
+
}),
|
|
224
|
+
})
|
|
154
225
|
.command({
|
|
155
226
|
command: 'create',
|
|
156
227
|
describe: 'Create a new flow (not yet implemented)',
|
|
@@ -187,6 +258,9 @@ export function flowsCmd(wrap) {
|
|
|
187
258
|
console.log(' list List flows');
|
|
188
259
|
console.log(' executions Show flow executions from sys_flow_context');
|
|
189
260
|
console.log(' show <identifier> Show flow details by name or sys_id');
|
|
261
|
+
console.log(' status <identifier> Check whether a flow is actually able to run');
|
|
262
|
+
console.log(' publish <identifier> Publish (activate) a flow');
|
|
263
|
+
console.log(' doctor Check whether this instance can publish flows');
|
|
190
264
|
console.log(' create Create a new flow (not yet implemented)');
|
|
191
265
|
console.log(' update <identifier> Update a flow (not yet implemented)');
|
|
192
266
|
console.log(' delete <identifier> Delete a flow (not yet implemented)');
|
|
@@ -199,823 +273,3 @@ export function flowsCmd(wrap) {
|
|
|
199
273
|
},
|
|
200
274
|
};
|
|
201
275
|
}
|
|
202
|
-
|
|
203
|
-
export async function formatFlowInspection(inspection, ctx) {
|
|
204
|
-
const lines = [];
|
|
205
|
-
const instanceURL = ctx?.instanceURL || '';
|
|
206
|
-
const flow = inspection.flow;
|
|
207
|
-
|
|
208
|
-
lines.push('');
|
|
209
|
-
lines.push(`Flow: ${flow.name}`);
|
|
210
|
-
lines.push('');
|
|
211
|
-
|
|
212
|
-
const status = flow.active ? 'Active' : 'Inactive';
|
|
213
|
-
let version = flow.version || inferFlowVersion(inspection);
|
|
214
|
-
lines.push(` Status: ${status} | Version: ${version}`);
|
|
215
|
-
lines.push(` Sys ID: ${flow.sysID}`);
|
|
216
|
-
if (instanceURL && flow.sysID) {
|
|
217
|
-
lines.push(` Link: ${instanceURL}/sys_hub_flow.do?sys_id=${flow.sysID}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Flow variables section
|
|
221
|
-
if (inspection.flowVariables && inspection.flowVariables.length > 0) {
|
|
222
|
-
const vars = [...inspection.flowVariables].sort((a, b) => {
|
|
223
|
-
const ao = parseInt(getStringField(a, 'order'), 10) || 0;
|
|
224
|
-
const bo = parseInt(getStringField(b, 'order'), 10) || 0;
|
|
225
|
-
return ao - bo;
|
|
226
|
-
});
|
|
227
|
-
lines.push('');
|
|
228
|
-
lines.push('▶ FLOW VARIABLES');
|
|
229
|
-
lines.push('─'.repeat(50));
|
|
230
|
-
for (const v of vars) {
|
|
231
|
-
const name = firstNonEmpty(getStringField(v, 'label'), getStringField(v, 'name'), 'Variable');
|
|
232
|
-
const typeName = firstNonEmpty(getStringField(v, 'type_label'), getStringField(v, 'type'), '');
|
|
233
|
-
if (typeName) {
|
|
234
|
-
lines.push(` • ${name}: ${typeName}`);
|
|
235
|
-
} else {
|
|
236
|
-
lines.push(` • ${name}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Subflow I/O section
|
|
242
|
-
if (flow.type && flow.type.toLowerCase() === 'subflow') {
|
|
243
|
-
lines.push('');
|
|
244
|
-
lines.push('▶ SUBFLOW');
|
|
245
|
-
lines.push('─'.repeat(50));
|
|
246
|
-
|
|
247
|
-
if (inspection.flowInputs && inspection.flowInputs.length > 0) {
|
|
248
|
-
lines.push(` Inputs (${inspection.flowInputs.length})`);
|
|
249
|
-
for (const input of inspection.flowInputs) {
|
|
250
|
-
const name = firstNonEmpty(getStringField(input, 'label'), getStringField(input, 'name'), 'Input');
|
|
251
|
-
const typeName = getStringField(input, 'type');
|
|
252
|
-
if (typeName) {
|
|
253
|
-
lines.push(` • ${name}: ${typeName}`);
|
|
254
|
-
} else {
|
|
255
|
-
lines.push(` • ${name}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (inspection.flowOutputs && inspection.flowOutputs.length > 0) {
|
|
261
|
-
if (inspection.flowInputs && inspection.flowInputs.length > 0) {
|
|
262
|
-
lines.push('');
|
|
263
|
-
}
|
|
264
|
-
lines.push(` Outputs (${inspection.flowOutputs.length})`);
|
|
265
|
-
for (const out of inspection.flowOutputs) {
|
|
266
|
-
const name = firstNonEmpty(getStringField(out, 'label'), getStringField(out, 'name'), 'Output');
|
|
267
|
-
const typeName = getStringField(out, 'type');
|
|
268
|
-
if (typeName) {
|
|
269
|
-
lines.push(` • ${name}: ${typeName}`);
|
|
270
|
-
} else {
|
|
271
|
-
lines.push(` • ${name}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Trigger section
|
|
278
|
-
const { name: triggerName, type: triggerType, table: triggerTable, time: triggerTime, condition: triggerCondition } = extractTriggerDetails(inspection);
|
|
279
|
-
if (triggerName || triggerType || triggerTable || triggerTime || triggerCondition) {
|
|
280
|
-
lines.push('');
|
|
281
|
-
lines.push('▶ TRIGGER');
|
|
282
|
-
lines.push('─'.repeat(50));
|
|
283
|
-
if (triggerName) lines.push(` Name: ${triggerName}`);
|
|
284
|
-
if (triggerType) lines.push(` Type: ${titleCase(triggerType.replace(/_/g, ' '))}`);
|
|
285
|
-
if (triggerTable) lines.push(` Table: ${triggerTable}`);
|
|
286
|
-
if (triggerTime) lines.push(` Time: ${triggerTime}`);
|
|
287
|
-
if (triggerCondition) lines.push(` Condition: ${formatTriggerCondition(triggerCondition)}`);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Flow structure section
|
|
291
|
-
lines.push('');
|
|
292
|
-
lines.push('⚡ FLOW STRUCTURE');
|
|
293
|
-
lines.push('─'.repeat(50));
|
|
294
|
-
const structureLines = await formatFlowStructure(inspection, ctx);
|
|
295
|
-
lines.push(...structureLines);
|
|
296
|
-
// Note limitation only when we couldn't reconstruct detail from the payload OR the gzip fallback
|
|
297
|
-
const hasPayload = inspection.payload && Object.keys(inspection.payload).length > 0;
|
|
298
|
-
const hasDecodedLogic = (inspection.flowLogicInstances || []).some(l => l._decodedValues);
|
|
299
|
-
if (!hasPayload && !hasDecodedLogic) {
|
|
300
|
-
lines.push('');
|
|
301
|
-
lines.push(' (step detail limited — no flow definition data available)');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
lines.push('');
|
|
305
|
-
return lines.join('\n') + '\n';
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function extractTriggerDetails(inspection) {
|
|
309
|
-
let name = '';
|
|
310
|
-
let type = '';
|
|
311
|
-
let table = '';
|
|
312
|
-
let time = '';
|
|
313
|
-
let condition = '';
|
|
314
|
-
|
|
315
|
-
if (inspection.version && typeof inspection.version === 'object') {
|
|
316
|
-
name = getStringField(inspection.version, 'trigger_name');
|
|
317
|
-
type = getStringField(inspection.version, 'trigger_type');
|
|
318
|
-
table = getStringField(inspection.version, 'trigger_table');
|
|
319
|
-
time = getStringField(inspection.version, 'trigger_time');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if ((!name || !type || !table || !time || !condition) && Object.keys(inspection.payload).length > 0) {
|
|
323
|
-
const triggers = inspection.payload.triggerInstances;
|
|
324
|
-
if (Array.isArray(triggers) && triggers.length > 0) {
|
|
325
|
-
const trigger = triggers[0];
|
|
326
|
-
if (trigger && typeof trigger === 'object') {
|
|
327
|
-
name = firstNonEmpty(name, getStringField(trigger, 'name'));
|
|
328
|
-
type = firstNonEmpty(type, getStringField(trigger, 'type'));
|
|
329
|
-
if (Array.isArray(trigger.inputs)) {
|
|
330
|
-
for (const input of trigger.inputs) {
|
|
331
|
-
if (!input || typeof input !== 'object') continue;
|
|
332
|
-
const k = getStringField(input, 'name');
|
|
333
|
-
const v = firstNonEmpty(getStringField(input, 'displayValue'), getStringField(input, 'value'));
|
|
334
|
-
if (k === 'table') table = firstNonEmpty(table, v);
|
|
335
|
-
if (k === 'time') time = firstNonEmpty(time, v);
|
|
336
|
-
if (k === 'condition') condition = firstNonEmpty(condition, v);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (!name && inspection.triggerInstances && inspection.triggerInstances.length > 0) {
|
|
344
|
-
const first = inspection.triggerInstances[0];
|
|
345
|
-
name = firstNonEmpty(name, getStringField(first, 'name'), getStringField(first, 'display_text'), getNestedString(first, 'trigger_definition', 'display_value'));
|
|
346
|
-
type = firstNonEmpty(type, getStringField(first, 'trigger_type'), getNestedString(first, 'trigger_definition', 'display_value'));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (time.includes(' ')) {
|
|
350
|
-
const parts = time.split(' ');
|
|
351
|
-
if (parts.length === 2) {
|
|
352
|
-
time = parts[1];
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return { name, type, table, time, condition };
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function inferFlowVersion(inspection) {
|
|
360
|
-
if (Object.keys(inspection.payload).length > 0) return 'Unset (Assumed V1)';
|
|
361
|
-
if (inspection.actionInstances && inspection.actionInstances.length > 0) return 'Unset (Assumed V1)';
|
|
362
|
-
return 'Unset';
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
async function formatFlowStructure(inspection, ctx) {
|
|
366
|
-
const names = await collectCatalogVarNames(inspection, ctx);
|
|
367
|
-
if (names.size > 0) {
|
|
368
|
-
if (!ctx.catalogVarNames) ctx.catalogVarNames = new Map();
|
|
369
|
-
for (const [k, v] of names) ctx.catalogVarNames.set(k, v);
|
|
370
|
-
}
|
|
371
|
-
const labelCache = buildLabelCache(inspection.payload);
|
|
372
|
-
if (labelCache.size > 0) {
|
|
373
|
-
if (!ctx.labelCache) ctx.labelCache = new Map();
|
|
374
|
-
for (const [k, v] of labelCache) ctx.labelCache.set(k, v);
|
|
375
|
-
}
|
|
376
|
-
const payload = inspection.payload;
|
|
377
|
-
if (Object.keys(payload).length > 0) {
|
|
378
|
-
return formatFlowStructureFromPayload(payload, ctx);
|
|
379
|
-
}
|
|
380
|
-
return formatFlowStructureFallback(inspection);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* The ProcessFlow payload carries a label cache (also serialized as
|
|
385
|
-
* labelCacheAsJsonString) mapping data-pill names like "<step-uid>.<var>"
|
|
386
|
-
* to the human labels the Flow Designer UI shows, e.g.
|
|
387
|
-
* "1 - Get Catalog Variables➛session_title". Build a Map so guid-prefixed
|
|
388
|
-
* pills can be rendered readably.
|
|
389
|
-
*/
|
|
390
|
-
function buildLabelCache(payload) {
|
|
391
|
-
const cache = new Map();
|
|
392
|
-
if (!payload || typeof payload !== 'object') return cache;
|
|
393
|
-
let entries = payload.label_cache;
|
|
394
|
-
if (!Array.isArray(entries)) {
|
|
395
|
-
try {
|
|
396
|
-
entries = JSON.parse(payload.labelCacheAsJsonString || '[]');
|
|
397
|
-
} catch {
|
|
398
|
-
entries = [];
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
for (const e of entries) {
|
|
402
|
-
if (!e || typeof e !== 'object') continue;
|
|
403
|
-
const name = getStringField(e, 'name');
|
|
404
|
-
const label = getStringField(e, 'label');
|
|
405
|
-
if (name && label) cache.set(name, label);
|
|
406
|
-
}
|
|
407
|
-
return cache;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const GUID_PILL_RE = /\{\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.([^}]+)\}\}/g;
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Replace opaque guid-prefixed pills ({{<32-hex-guid>.<var>}}) with their
|
|
414
|
-
* human label from the payload's label cache. Readable step refs like
|
|
415
|
-
* {{Created_1.table_name}} are NOT matched and stay raw.
|
|
416
|
-
*/
|
|
417
|
-
function resolveGuidPills(value, labelCache) {
|
|
418
|
-
if (!labelCache || labelCache.size === 0 || !value) return value;
|
|
419
|
-
return String(value).replace(GUID_PILL_RE, (m, guid, name) => {
|
|
420
|
-
const label = labelCache.get(`${guid}.${name}`);
|
|
421
|
-
return label ? `{{${label}}}` : m;
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* ServiceNow serializes catalog variable selections in action inputs as
|
|
427
|
-
* comma-separated "sys_id:table" pairs (e.g. Get Catalog Variables / Create
|
|
428
|
-
* Catalog Task steps). The payload carries the source table on the parameter
|
|
429
|
-
* attributes, but the value itself also names it in each token. Resolve those
|
|
430
|
-
* sys_ids to readable labels (question_text, falling back to name) with one
|
|
431
|
-
* batched Table API query per distinct table.
|
|
432
|
-
*/
|
|
433
|
-
async function collectCatalogVarNames(inspection, ctx) {
|
|
434
|
-
const names = new Map();
|
|
435
|
-
if (!ctx?.sdk) return names;
|
|
436
|
-
|
|
437
|
-
const refs = new Map(); // table -> Set<sys_id>
|
|
438
|
-
const TOKEN_RE = /([0-9a-f]{32}):([a-z_]+)/g;
|
|
439
|
-
|
|
440
|
-
function addRefsFromValue(value) {
|
|
441
|
-
if (!value) return;
|
|
442
|
-
for (const m of String(value).matchAll(TOKEN_RE)) {
|
|
443
|
-
const [, id, table] = m;
|
|
444
|
-
if (!refs.has(table)) refs.set(table, new Set());
|
|
445
|
-
refs.get(table).add(id);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function walk(items) {
|
|
450
|
-
if (!Array.isArray(items)) return;
|
|
451
|
-
for (const item of items) {
|
|
452
|
-
if (!item || typeof item !== 'object') continue;
|
|
453
|
-
if (Array.isArray(item.inputs)) {
|
|
454
|
-
for (const input of item.inputs) {
|
|
455
|
-
if (!input || typeof input !== 'object') continue;
|
|
456
|
-
addRefsFromValue(firstNonEmpty(getStringField(input, 'displayValue'), getStringField(input, 'value')));
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
if (Array.isArray(item.flowBlock)) walk(item.flowBlock);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const payload = inspection?.payload;
|
|
464
|
-
if (payload && typeof payload === 'object') {
|
|
465
|
-
walk(payload.actionInstances);
|
|
466
|
-
if (Array.isArray(payload.flowLogicInstances)) {
|
|
467
|
-
for (const logic of payload.flowLogicInstances) walk(logic?.flowBlock);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
// Fallback table sources may also carry inputs (e.g. decoded logic values)
|
|
471
|
-
walk(inspection?.actionInstances);
|
|
472
|
-
if (Array.isArray(inspection?.flowLogicInstances)) {
|
|
473
|
-
for (const logic of inspection.flowLogicInstances) {
|
|
474
|
-
if (logic?._decodedValues?.inputs) {
|
|
475
|
-
for (const input of logic._decodedValues.inputs) {
|
|
476
|
-
if (!input || typeof input !== 'object') continue;
|
|
477
|
-
addRefsFromValue(firstNonEmpty(getStringField(input, 'displayValue'), getStringField(input, 'value')));
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
for (const [table, ids] of refs) {
|
|
484
|
-
const idList = [...ids];
|
|
485
|
-
for (let i = 0; i < idList.length; i += 100) {
|
|
486
|
-
const chunk = idList.slice(i, i + 100);
|
|
487
|
-
try {
|
|
488
|
-
const params = new URLSearchParams();
|
|
489
|
-
params.set('sysparm_query', `sys_idIN${chunk.join(',')}`);
|
|
490
|
-
params.set('sysparm_fields', 'sys_id,name,question_text');
|
|
491
|
-
params.set('sysparm_display_value', 'all');
|
|
492
|
-
params.set('sysparm_limit', String(chunk.length));
|
|
493
|
-
const records = await ctx.sdk.list(table, params);
|
|
494
|
-
for (const r of records) {
|
|
495
|
-
const id = getStringField(r, 'sys_id');
|
|
496
|
-
const label = firstNonEmpty(getStringField(r, 'question_text'), getStringField(r, 'name'));
|
|
497
|
-
if (id && label) names.set(id, label);
|
|
498
|
-
}
|
|
499
|
-
} catch {
|
|
500
|
-
// resolution failed (no access, deleted record) — leave raw sys_ids
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return names;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Replace catalog variable "sys_id:table" pairs with readable labels when the
|
|
510
|
-
* resolver map has them. Returns null when the value isn't a catalog-variable
|
|
511
|
-
* list (so callers keep rendering it raw).
|
|
512
|
-
*/
|
|
513
|
-
function resolveCatalogVarValue(value, catalogVarNames) {
|
|
514
|
-
if (!catalogVarNames || catalogVarNames.size === 0 || !value) return null;
|
|
515
|
-
const trimmed = String(value).trim();
|
|
516
|
-
if (!/^([0-9a-f]{32}:[a-z_]+)(,[0-9a-f]{32}:[a-z_]+)*,?$/.test(trimmed)) return null;
|
|
517
|
-
return String(value).replace(/([0-9a-f]{32}):[a-z_]+/g, (m, id) => {
|
|
518
|
-
const label = catalogVarNames.get(id);
|
|
519
|
-
return label || m;
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
async function formatFlowStructureFromPayload(payload, ctx) {
|
|
524
|
-
const childUIDs = new Set();
|
|
525
|
-
|
|
526
|
-
function markChildren(items) {
|
|
527
|
-
if (!Array.isArray(items)) return;
|
|
528
|
-
for (const item of items) {
|
|
529
|
-
if (!item || typeof item !== 'object') continue;
|
|
530
|
-
const uid = getStringField(item, 'uiUniqueIdentifier');
|
|
531
|
-
if (uid) childUIDs.add(uid);
|
|
532
|
-
if (Array.isArray(item.flowBlock)) {
|
|
533
|
-
markChildren(item.flowBlock);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (Array.isArray(payload.flowLogicInstances)) {
|
|
539
|
-
for (const logic of payload.flowLogicInstances) {
|
|
540
|
-
if (!logic || typeof logic !== 'object') continue;
|
|
541
|
-
if (Array.isArray(logic.flowBlock)) {
|
|
542
|
-
markChildren(logic.flowBlock);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const roots = [];
|
|
548
|
-
|
|
549
|
-
function addFromPayload(key, stepType) {
|
|
550
|
-
const items = payload[key];
|
|
551
|
-
if (!Array.isArray(items)) return;
|
|
552
|
-
for (const item of items) {
|
|
553
|
-
if (!item || typeof item !== 'object') continue;
|
|
554
|
-
const uid = getStringField(item, 'uiUniqueIdentifier');
|
|
555
|
-
if (uid && childUIDs.has(uid)) continue;
|
|
556
|
-
roots.push({ stepType, data: item, order: parseOrderField(item) });
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
addFromPayload('actionInstances', 'action');
|
|
561
|
-
addFromPayload('subFlowInstances', 'subflow');
|
|
562
|
-
addFromPayload('flowLogicInstances', 'logic');
|
|
563
|
-
|
|
564
|
-
roots.sort((a, b) => a.order - b.order);
|
|
565
|
-
|
|
566
|
-
if (roots.length === 0) {
|
|
567
|
-
return [' (no steps found)'];
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const lines = [];
|
|
571
|
-
let stepNum = 1;
|
|
572
|
-
|
|
573
|
-
async function walk(steps, indent) {
|
|
574
|
-
for (const step of steps) {
|
|
575
|
-
const pad = ' '.repeat(indent);
|
|
576
|
-
lines.push(...await formatStepLine(stepNum, pad, step, ctx));
|
|
577
|
-
stepNum++;
|
|
578
|
-
|
|
579
|
-
if (step.stepType !== 'logic') continue;
|
|
580
|
-
const block = step.data.flowBlock;
|
|
581
|
-
if (!Array.isArray(block) || block.length === 0) continue;
|
|
582
|
-
|
|
583
|
-
const children = [];
|
|
584
|
-
for (const raw of block) {
|
|
585
|
-
if (!raw || typeof raw !== 'object') continue;
|
|
586
|
-
children.push({
|
|
587
|
-
stepType: classifyPayloadItem(raw),
|
|
588
|
-
data: raw,
|
|
589
|
-
order: parseOrderField(raw),
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
children.sort((a, b) => a.order - b.order);
|
|
593
|
-
await walk(children, indent + 1);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
await walk(roots, 0);
|
|
598
|
-
return lines;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function formatFlowStructureFallback(inspection) {
|
|
602
|
-
const steps = [];
|
|
603
|
-
|
|
604
|
-
if (inspection.actionInstances) {
|
|
605
|
-
for (const action of inspection.actionInstances) {
|
|
606
|
-
const name = firstNonEmpty(
|
|
607
|
-
getNestedString(action, 'action_type', 'display_value'),
|
|
608
|
-
getStringField(action, 'name'),
|
|
609
|
-
getStringField(action, 'display_text'),
|
|
610
|
-
'Action',
|
|
611
|
-
);
|
|
612
|
-
steps.push({ order: parseOrderField(action), text: name, comment: getStringField(action, 'comment') });
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (inspection.flowLogicInstances) {
|
|
617
|
-
for (const logic of inspection.flowLogicInstances) {
|
|
618
|
-
const name = firstNonEmpty(
|
|
619
|
-
getNestedString(logic, 'logic_definition', 'display_value'),
|
|
620
|
-
getStringField(logic, 'name'),
|
|
621
|
-
getStringField(logic, 'display_text'),
|
|
622
|
-
'Logic',
|
|
623
|
-
);
|
|
624
|
-
steps.push({
|
|
625
|
-
order: parseOrderField(logic),
|
|
626
|
-
text: name,
|
|
627
|
-
comment: getStringField(logic, 'comment'),
|
|
628
|
-
decoded: logic._decodedValues || null,
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
if (inspection.subFlowInstances) {
|
|
634
|
-
for (const sf of inspection.subFlowInstances) {
|
|
635
|
-
const name = firstNonEmpty(
|
|
636
|
-
getNestedString(sf, 'subflow', 'display_value'),
|
|
637
|
-
getStringField(sf, 'name'),
|
|
638
|
-
getStringField(sf, 'display_text'),
|
|
639
|
-
'Subflow',
|
|
640
|
-
);
|
|
641
|
-
steps.push({ order: parseOrderField(sf), text: '↪ ' + name });
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
steps.sort((a, b) => a.order - b.order);
|
|
646
|
-
|
|
647
|
-
if (steps.length === 0) {
|
|
648
|
-
return [' (no steps found)'];
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const lines = [];
|
|
652
|
-
steps.forEach((step, i) => {
|
|
653
|
-
const num = `${i + 1}.`;
|
|
654
|
-
// Decoded gzip values carry the same input detail as a V2 payload
|
|
655
|
-
if (step.decoded && (step.text === 'If' || step.text === 'Else If')) {
|
|
656
|
-
let condition = '';
|
|
657
|
-
let conditionLabel = '';
|
|
658
|
-
if (Array.isArray(step.decoded.inputs)) {
|
|
659
|
-
for (const raw of step.decoded.inputs) {
|
|
660
|
-
if (!raw || typeof raw !== 'object') continue;
|
|
661
|
-
const inputName = getStringField(raw, 'name');
|
|
662
|
-
if (inputName === 'condition') {
|
|
663
|
-
condition = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
664
|
-
}
|
|
665
|
-
if (inputName === 'condition_name') {
|
|
666
|
-
conditionLabel = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
let displayText = step.text;
|
|
671
|
-
if (conditionLabel) {
|
|
672
|
-
displayText = `${step.text}: ${conditionLabel}`;
|
|
673
|
-
} else if (condition && condition.length < 60) {
|
|
674
|
-
displayText = `${step.text}: ${condition}`;
|
|
675
|
-
}
|
|
676
|
-
lines.push(` ${num} ${displayText}`);
|
|
677
|
-
if (condition && condition.length >= 60 && !conditionLabel) {
|
|
678
|
-
lines.push(` Condition: ${condition}`);
|
|
679
|
-
}
|
|
680
|
-
} else if (step.decoded && step.text === 'Set Flow Variables') {
|
|
681
|
-
lines.push(` ${num} ${step.text}`);
|
|
682
|
-
const vars = step.decoded.variables || step.decoded.flowVariables;
|
|
683
|
-
if (Array.isArray(vars) && vars.length > 0) {
|
|
684
|
-
lines.push(` Variables Set:`);
|
|
685
|
-
for (const raw of vars) {
|
|
686
|
-
if (!raw || typeof raw !== 'object') continue;
|
|
687
|
-
const varName = getStringField(raw, 'name');
|
|
688
|
-
const varValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
689
|
-
if (!varName) continue;
|
|
690
|
-
lines.push(varValue ? ` • ${varName} = ${varValue}` : ` • ${varName}`);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
} else {
|
|
694
|
-
lines.push(` ${num} ${step.text}`);
|
|
695
|
-
}
|
|
696
|
-
if (step.comment) {
|
|
697
|
-
lines.push(` Annotation: ${step.comment}`);
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
return lines;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
async function formatStepLine(stepNum, pad, step, ctx) {
|
|
705
|
-
switch (step.stepType) {
|
|
706
|
-
case 'logic':
|
|
707
|
-
return formatLogicStep(stepNum, pad, step.data);
|
|
708
|
-
case 'subflow':
|
|
709
|
-
return formatSubFlowStep(stepNum, pad, step.data, ctx);
|
|
710
|
-
default:
|
|
711
|
-
return formatActionStepWithChildren(stepNum, pad, step.data, ctx);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
async function formatActionStepWithChildren(stepNum, pad, action, ctx) {
|
|
716
|
-
const lines = formatActionStep(stepNum, pad, action, ctx);
|
|
717
|
-
const depth = ctx?.depth ?? 1;
|
|
718
|
-
if (!ctx?.sdk?.inspectCustomAction || depth <= 1) return lines;
|
|
719
|
-
|
|
720
|
-
const actionName = getActionName(action);
|
|
721
|
-
if (!actionName) return lines;
|
|
722
|
-
|
|
723
|
-
if (!ctx.customActionCache) ctx.customActionCache = new Map();
|
|
724
|
-
if (!ctx.customActionCache.has(actionName)) {
|
|
725
|
-
ctx.customActionCache.set(actionName, ctx.sdk.inspectCustomAction(actionName).catch(() => null));
|
|
726
|
-
}
|
|
727
|
-
const details = await ctx.customActionCache.get(actionName);
|
|
728
|
-
const steps = details?.steps;
|
|
729
|
-
if (!Array.isArray(steps) || steps.length === 0 || !hasUsefulActionSteps(actionName, steps)) return lines;
|
|
730
|
-
|
|
731
|
-
lines.push(`${pad} ↳ Internal action steps`);
|
|
732
|
-
const ordered = [...steps].sort((a, b) => parseOrderField(a) - parseOrderField(b));
|
|
733
|
-
ordered.forEach((step, index) => {
|
|
734
|
-
const label = firstNonEmpty(getStringField(step, 'label'), getStringField(step, 'name'), 'Action step');
|
|
735
|
-
const type = getNestedString(step, 'step_type', 'display_value') || getStringField(step, 'step_type');
|
|
736
|
-
const suffix = type ? ` - ${type}` : '';
|
|
737
|
-
lines.push(`${pad} Step ${index + 1}: ${label}${suffix}`);
|
|
738
|
-
appendStepValues(lines, pad + ' ', step, ctx);
|
|
739
|
-
});
|
|
740
|
-
return lines;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function hasUsefulActionSteps(actionName, steps) {
|
|
744
|
-
if (steps.length !== 1) return true;
|
|
745
|
-
const step = steps[0];
|
|
746
|
-
const label = firstNonEmpty(getStringField(step, 'label'), getStringField(step, 'name'))
|
|
747
|
-
.replace(/\s+step$/i, '').trim().toLowerCase();
|
|
748
|
-
return label !== actionName.trim().toLowerCase();
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function appendStepValues(lines, pad, step, ctx) {
|
|
752
|
-
for (const field of ['inputs', 'outputs', 'extended_inputs', 'extended_outputs']) {
|
|
753
|
-
const value = getStringField(step, field);
|
|
754
|
-
if (!value) continue;
|
|
755
|
-
const resolved = resolveGuidPills(value, ctx?.labelCache);
|
|
756
|
-
for (const part of resolved.split('^')) {
|
|
757
|
-
if (part) lines.push(`${pad}${field}: ${part}`);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function getActionName(action) {
|
|
763
|
-
return firstNonEmpty(
|
|
764
|
-
getNestedString(action, 'actionType', 'fName'),
|
|
765
|
-
getStringField(action, 'actionName'),
|
|
766
|
-
getStringField(action, 'actionInternalName'),
|
|
767
|
-
getStringField(action, 'name'),
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
export function formatActionStep(stepNum, pad, action, ctx) {
|
|
772
|
-
const lines = [];
|
|
773
|
-
let actionName = firstNonEmpty(
|
|
774
|
-
getNestedString(action, 'actionType', 'fName'),
|
|
775
|
-
getStringField(action, 'actionName'),
|
|
776
|
-
getStringField(action, 'actionInternalName'),
|
|
777
|
-
getStringField(action, 'name'),
|
|
778
|
-
'Unknown Action',
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
const idx = actionName.indexOf(' : ');
|
|
782
|
-
if (idx > 0) {
|
|
783
|
-
actionName = actionName.slice(idx + 3).trim();
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
let tableName = '';
|
|
787
|
-
if (Array.isArray(action.inputs)) {
|
|
788
|
-
for (const raw of action.inputs) {
|
|
789
|
-
if (!raw || typeof raw !== 'object') continue;
|
|
790
|
-
if (getStringField(raw, 'name') === 'table_name') {
|
|
791
|
-
tableName = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
792
|
-
break;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const showsTableSuffix = tableName && (actionName === 'Update Record' || actionName === 'Create or Update Record');
|
|
798
|
-
let actionDisplay = actionName;
|
|
799
|
-
if (showsTableSuffix) {
|
|
800
|
-
actionDisplay = actionName + ' - ' + tableName;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const comment = firstNonEmpty(getStringField(action, 'comment'), getStringField(action, 'displayText'));
|
|
804
|
-
if (comment) {
|
|
805
|
-
lines.push(`${pad}${stepNum}. ${actionDisplay} (${comment})`);
|
|
806
|
-
} else {
|
|
807
|
-
lines.push(`${pad}${stepNum}. ${actionDisplay}`);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (Array.isArray(action.inputs)) {
|
|
811
|
-
for (const raw of action.inputs) {
|
|
812
|
-
if (!raw || typeof raw !== 'object') continue;
|
|
813
|
-
const inputName = getStringField(raw, 'name');
|
|
814
|
-
if (inputName === 'table_name' && showsTableSuffix) continue;
|
|
815
|
-
|
|
816
|
-
let inputValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
817
|
-
|
|
818
|
-
// Catalog variable selections arrive as "sys_id:table" pairs; resolve
|
|
819
|
-
// them to readable names when we have a resolver map (built per flow).
|
|
820
|
-
const resolvedCatalog = resolveCatalogVarValue(inputValue, ctx?.catalogVarNames);
|
|
821
|
-
if (resolvedCatalog) inputValue = resolvedCatalog;
|
|
822
|
-
|
|
823
|
-
// Opaque guid-prefixed pills ({{<32-hex>.<var>}}) get their human
|
|
824
|
-
// label from the payload's label cache. Readable step refs stay raw.
|
|
825
|
-
inputValue = resolveGuidPills(inputValue, ctx?.labelCache);
|
|
826
|
-
|
|
827
|
-
let label = inputName;
|
|
828
|
-
if (raw.parameter && typeof raw.parameter === 'object') {
|
|
829
|
-
label = firstNonEmpty(getStringField(raw.parameter, 'label'), label);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
if (!inputValue) {
|
|
833
|
-
lines.push(`${pad} ${label}: (not set)`);
|
|
834
|
-
continue;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// ServiceNow writes multi-field payloads (encoded-query style) as
|
|
838
|
-
// "field=value^field2=value2". Split those onto one line per field;
|
|
839
|
-
// plain long strings stay on a single line, untruncated.
|
|
840
|
-
if (inputValue.includes('^')) {
|
|
841
|
-
for (const field of inputValue.split('^')) {
|
|
842
|
-
if (field) lines.push(`${pad} ${label}: ${field}`);
|
|
843
|
-
}
|
|
844
|
-
} else {
|
|
845
|
-
lines.push(`${pad} ${label}: ${inputValue}`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
return lines;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
export async function formatSubFlowStep(stepNum, pad, subFlow, ctx) {
|
|
854
|
-
const lines = [];
|
|
855
|
-
const subFlowName = firstNonEmpty(
|
|
856
|
-
getNestedString(subFlow, 'subFlowType', 'fName'),
|
|
857
|
-
getStringField(subFlow, 'subFlowName'),
|
|
858
|
-
getStringField(subFlow, 'subFlowInternalName'),
|
|
859
|
-
getNestedString(subFlow, 'subFlow', 'name'),
|
|
860
|
-
getStringField(subFlow, 'name'),
|
|
861
|
-
'Unknown Subflow',
|
|
862
|
-
);
|
|
863
|
-
|
|
864
|
-
const comment = getStringField(subFlow, 'comment');
|
|
865
|
-
if (comment) {
|
|
866
|
-
lines.push(`${pad}${stepNum}. ↪ ${subFlowName} (${comment})`);
|
|
867
|
-
} else {
|
|
868
|
-
lines.push(`${pad}${stepNum}. ↪ ${subFlowName}`);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// Recurse into the subflow when depth allows and we haven't already seen it.
|
|
872
|
-
// Note: `subflowSysId` on the instance is a snapshot id (sys_hub_flow_snapshot),
|
|
873
|
-
// NOT the flow id. The embedded `subFlow.parentFlow` is the real flow sys_id.
|
|
874
|
-
const subflowSysId = firstNonEmpty(
|
|
875
|
-
getNestedString(subFlow, 'subFlow', 'parentFlow'),
|
|
876
|
-
getStringField(subFlow, 'subflowSysId'),
|
|
877
|
-
getNestedString(subFlow, 'subFlow', 'sys_id'),
|
|
878
|
-
getStringField(subFlow, 'sys_id'),
|
|
879
|
-
);
|
|
880
|
-
const depth = ctx?.depth ?? 1;
|
|
881
|
-
const canRecurse = ctx?.sdk && subflowSysId && depth > 1 && !ctx.visited.has(subflowSysId);
|
|
882
|
-
|
|
883
|
-
if (!canRecurse) {
|
|
884
|
-
if (!subflowSysId) {
|
|
885
|
-
lines.push(`${pad} (subflow definition not found)`);
|
|
886
|
-
} else if (depth <= 1) {
|
|
887
|
-
lines.push(`${pad} jsn flows show "${subFlowName}"`);
|
|
888
|
-
}
|
|
889
|
-
return lines;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
ctx.visited.add(subflowSysId);
|
|
893
|
-
try {
|
|
894
|
-
const subInspection = await ctx.sdk.inspectFlow(subflowSysId);
|
|
895
|
-
const subCtx = { ...ctx, depth: depth - 1 };
|
|
896
|
-
const subLines = await formatFlowStructure(subInspection, subCtx);
|
|
897
|
-
if (subLines.length === 0) {
|
|
898
|
-
lines.push(`${pad} (no steps found)`);
|
|
899
|
-
} else {
|
|
900
|
-
const innerPad = pad + ' ';
|
|
901
|
-
for (const l of subLines) {
|
|
902
|
-
lines.push(innerPad + l.replace(/^ {2}/, ''));
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
} catch (e) {
|
|
906
|
-
lines.push(`${pad} (could not load subflow: ${e.message})`);
|
|
907
|
-
}
|
|
908
|
-
return lines;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
function formatLogicStep(stepNum, pad, logic) {
|
|
912
|
-
const lines = [];
|
|
913
|
-
const logicType = firstNonEmpty(getNestedString(logic, 'flowLogicDefinition', 'name'), getStringField(logic, 'name'), 'Logic Step');
|
|
914
|
-
|
|
915
|
-
const comment = getStringField(logic, 'comment');
|
|
916
|
-
let condition = '';
|
|
917
|
-
let conditionLabel = '';
|
|
918
|
-
|
|
919
|
-
if (logicType === 'If' || logicType === 'Else If') {
|
|
920
|
-
if (Array.isArray(logic.inputs)) {
|
|
921
|
-
for (const raw of logic.inputs) {
|
|
922
|
-
if (!raw || typeof raw !== 'object') continue;
|
|
923
|
-
const inputName = getStringField(raw, 'name');
|
|
924
|
-
if (inputName === 'condition') {
|
|
925
|
-
condition = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
926
|
-
}
|
|
927
|
-
if (inputName === 'condition_name') {
|
|
928
|
-
conditionLabel = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
let displayText = logicType;
|
|
935
|
-
if (conditionLabel) {
|
|
936
|
-
displayText = logicType + ': ' + conditionLabel;
|
|
937
|
-
} else if (condition && condition.length < 60) {
|
|
938
|
-
displayText = logicType + ': ' + condition;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
lines.push(`${pad}${stepNum}. ${displayText}`);
|
|
942
|
-
|
|
943
|
-
if (condition && condition.length >= 60 && !conditionLabel) {
|
|
944
|
-
lines.push(`${pad} Condition: ${condition}`);
|
|
945
|
-
}
|
|
946
|
-
if (comment) {
|
|
947
|
-
lines.push(`${pad} Annotation: ${comment}`);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
if (logicType === 'Set Flow Variables') {
|
|
951
|
-
if (Array.isArray(logic.flowVariables) && logic.flowVariables.length > 0) {
|
|
952
|
-
lines.push(`${pad} Variables Set:`);
|
|
953
|
-
for (const raw of logic.flowVariables) {
|
|
954
|
-
if (!raw || typeof raw !== 'object') continue;
|
|
955
|
-
const varName = getStringField(raw, 'name');
|
|
956
|
-
const varValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
957
|
-
if (!varName) continue;
|
|
958
|
-
if (varValue) {
|
|
959
|
-
lines.push(`${pad} • ${varName} = ${varValue}`);
|
|
960
|
-
} else {
|
|
961
|
-
lines.push(`${pad} • ${varName}`);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
return lines;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function classifyPayloadItem(m) {
|
|
971
|
-
if (m.flowLogicDefinition) return 'logic';
|
|
972
|
-
if (m.subFlowType) return 'subflow';
|
|
973
|
-
if (m.subflowSysId) return 'subflow';
|
|
974
|
-
if (m.subFlow) return 'subflow';
|
|
975
|
-
return 'action';
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function getNestedString(record, parent, field) {
|
|
979
|
-
const node = record?.[parent];
|
|
980
|
-
if (!node || typeof node !== 'object') return '';
|
|
981
|
-
return getStringField(node, field);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function firstNonEmpty(...values) {
|
|
985
|
-
for (const v of values) {
|
|
986
|
-
if (v && String(v).trim() !== '') return String(v).trim();
|
|
987
|
-
}
|
|
988
|
-
return '';
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function titleCase(s) {
|
|
992
|
-
return s
|
|
993
|
-
.toLowerCase()
|
|
994
|
-
.split(/\s+/)
|
|
995
|
-
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
996
|
-
.join(' ');
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
function formatTriggerCondition(condition) {
|
|
1000
|
-
if (!condition) return '';
|
|
1001
|
-
let result = condition;
|
|
1002
|
-
result = result.replace(/\^OR/g, ' OR ');
|
|
1003
|
-
result = result.replace(/\^/g, ' AND ');
|
|
1004
|
-
result = result.replace(/!=/g, ' != ');
|
|
1005
|
-
result = result.replace(/>=/g, ' >= ');
|
|
1006
|
-
result = result.replace(/<=/g, ' <= ');
|
|
1007
|
-
result = result.replace(/=/g, ' = ');
|
|
1008
|
-
result = result.replace(/>/g, ' > ');
|
|
1009
|
-
result = result.replace(/</g, ' < ');
|
|
1010
|
-
result = result.replace(/LIKE/g, ' LIKE ');
|
|
1011
|
-
while (result.includes(' ')) {
|
|
1012
|
-
result = result.replace(/ {2}/g, ' ');
|
|
1013
|
-
}
|
|
1014
|
-
return result.trim();
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function parseOrderField(record) {
|
|
1018
|
-
const order = getStringField(record, 'order');
|
|
1019
|
-
const n = parseInt(order, 10);
|
|
1020
|
-
return isNaN(n) ? 0 : n;
|
|
1021
|
-
}
|