@jacebenson/jsn 1.0.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -243
- package/bin/jsn.js +2 -57
- package/package.json +35 -32
- package/skills/servicenow/SKILL.md +137 -0
- package/src/app.js +172 -0
- package/src/auth.js +517 -0
- package/src/cli.js +174 -0
- package/src/commands/_ticket.js +260 -0
- package/src/commands/auth.js +227 -0
- package/src/commands/catalog.js +325 -0
- package/src/commands/changes.js +7 -0
- package/src/commands/dev/_generic.js +342 -0
- package/src/commands/dev/_simple.js +89 -0
- package/src/commands/dev/eval.js +38 -0
- package/src/commands/dev/flows.js +573 -0
- package/src/commands/dev/forms.js +313 -0
- package/src/commands/dev/lists.js +233 -0
- package/src/commands/dev/logs.js +64 -0
- package/src/commands/dev/rest.js +64 -0
- package/src/commands/dev/scopes.js +156 -0
- package/src/commands/dev/updatesets.js +184 -0
- package/src/commands/dev.js +53 -0
- package/src/commands/groupmembers.js +152 -0
- package/src/commands/grouproles.js +152 -0
- package/src/commands/groups.js +141 -0
- package/src/commands/incidents.js +7 -0
- package/src/commands/profiles.js +91 -0
- package/src/commands/records.js +171 -0
- package/src/commands/requests.js +59 -0
- package/src/commands/setup.js +39 -0
- package/src/commands/skill.js +144 -0
- package/src/commands/tasks.js +7 -0
- package/src/commands/tickets.js +121 -0
- package/src/commands/users.js +144 -0
- package/src/commands/version.js +81 -0
- package/src/config.js +154 -0
- package/src/context.js +62 -0
- package/src/errors.js +101 -0
- package/src/help.js +85 -0
- package/src/helpers.js +182 -0
- package/src/output.js +440 -0
- package/src/sdk.js +615 -0
- package/scripts/install.sh +0 -227
- package/scripts/npm-install.js +0 -235
- package/scripts/pre-commit-check.sh +0 -61
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
|
+
|
|
3
|
+
export function flowsCmd(wrap) {
|
|
4
|
+
return {
|
|
5
|
+
command: 'flows [subcommand]',
|
|
6
|
+
aliases: ['flow'],
|
|
7
|
+
describe: 'Manage Flow Designer flows',
|
|
8
|
+
builder: (yargs) => {
|
|
9
|
+
return yargs
|
|
10
|
+
.command({
|
|
11
|
+
command: 'list',
|
|
12
|
+
aliases: ['ls'],
|
|
13
|
+
describe: 'List flows',
|
|
14
|
+
builder: (y) => y
|
|
15
|
+
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
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: 20, describe: 'Max records' }),
|
|
18
|
+
handler: wrap(async (argv, app) => {
|
|
19
|
+
const columns = argv.columns ? argv.columns.split(',') : ['name', 'active', 'description', 'sys_created_by', 'sys_updated_on'];
|
|
20
|
+
const query = argv.query || '';
|
|
21
|
+
|
|
22
|
+
// Interactive picker
|
|
23
|
+
const picked = await interactiveList({
|
|
24
|
+
app, table: 'sys_hub_flow', singular: 'flow', columns, limit: argv.limit, query, labelField: 'name',
|
|
25
|
+
formatLabel: r => `${getStringField(r, 'name')} ${getStringField(r, 'active') === 'true' ? '' : '[inactive]'}`,
|
|
26
|
+
});
|
|
27
|
+
if (picked) {
|
|
28
|
+
const inspection = await app.sdk.inspectFlow(picked.sys_id);
|
|
29
|
+
const formatted = formatFlowInspection(inspection, app.getEffectiveInstance());
|
|
30
|
+
return app.ok({ ...inspection, _formatted: formatted }, {
|
|
31
|
+
summary: `Flow: ${inspection.flow.name}`,
|
|
32
|
+
breadcrumbs: [{ action: 'list', cmd: 'jsn dev flows list', description: 'Back to all flows' }],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Text/table fallback
|
|
37
|
+
const params = new URLSearchParams();
|
|
38
|
+
params.set('sysparm_limit', String(argv.limit));
|
|
39
|
+
params.set('sysparm_display_value', 'all');
|
|
40
|
+
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
41
|
+
const q = argv.query ? argv.query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
|
|
42
|
+
params.set('sysparm_query', q);
|
|
43
|
+
const records = await app.sdk.list('sys_hub_flow', params);
|
|
44
|
+
app.ok({
|
|
45
|
+
table: 'sys_hub_flow',
|
|
46
|
+
count: records.length,
|
|
47
|
+
columns,
|
|
48
|
+
records: records.map(r => formatRecordForDisplay(r, columns)),
|
|
49
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
50
|
+
}, { summary: `${records.length} flow(s)` });
|
|
51
|
+
}),
|
|
52
|
+
})
|
|
53
|
+
.command({
|
|
54
|
+
command: 'show <identifier>',
|
|
55
|
+
aliases: ['get'],
|
|
56
|
+
describe: 'Show flow details by name or sys_id',
|
|
57
|
+
handler: wrap(async (argv, app) => {
|
|
58
|
+
const inspection = await app.sdk.inspectFlow(argv.identifier);
|
|
59
|
+
const formatted = formatFlowInspection(inspection, app.getEffectiveInstance());
|
|
60
|
+
const data = { ...inspection, _formatted: formatted };
|
|
61
|
+
app.ok(data, {
|
|
62
|
+
summary: `Flow: ${inspection.flow.name}`,
|
|
63
|
+
breadcrumbs: [
|
|
64
|
+
{ action: 'list', cmd: 'jsn dev flows list', description: 'Back to all flows' },
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
.command({
|
|
70
|
+
command: 'create',
|
|
71
|
+
describe: 'Create a new flow (not yet implemented)',
|
|
72
|
+
builder: (y) => y
|
|
73
|
+
.option('data', { type: 'string', describe: 'JSON data for the flow' }),
|
|
74
|
+
handler: wrap(async (_argv, _app) => {
|
|
75
|
+
throw new Error('Flow creation requires the Flow Designer GraphQL API - not yet implemented.\n'
|
|
76
|
+
+ 'Use the ServiceNow web UI to create flows, then use "jsn dev flows list" to view them.');
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
.command({
|
|
80
|
+
command: 'update <identifier>',
|
|
81
|
+
describe: 'Update a flow (not yet implemented)',
|
|
82
|
+
builder: (y) => y
|
|
83
|
+
.option('data', { type: 'string', describe: 'JSON data to update' }),
|
|
84
|
+
handler: wrap(async (_argv, _app) => {
|
|
85
|
+
throw new Error('Flow updates require the Flow Designer GraphQL API - not yet implemented.\n'
|
|
86
|
+
+ 'Use the ServiceNow web UI to update flows, then use "jsn dev flows list" to view them.');
|
|
87
|
+
}),
|
|
88
|
+
})
|
|
89
|
+
.command({
|
|
90
|
+
command: 'delete <identifier>',
|
|
91
|
+
describe: 'Delete a flow (not yet implemented)',
|
|
92
|
+
handler: wrap(async (_argv, _app) => {
|
|
93
|
+
throw new Error('Flow deletion requires the Flow Designer GraphQL API - not yet implemented.\n'
|
|
94
|
+
+ 'Use the ServiceNow web UI to delete flows.');
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
handler: () => {},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatFlowInspection(inspection, instanceURL) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
const flow = inspection.flow;
|
|
105
|
+
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(`Flow: ${flow.name}`);
|
|
108
|
+
lines.push('');
|
|
109
|
+
|
|
110
|
+
const status = flow.active ? 'Active' : 'Inactive';
|
|
111
|
+
let version = flow.version || inferFlowVersion(inspection);
|
|
112
|
+
lines.push(` Status: ${status} | Version: ${version}`);
|
|
113
|
+
lines.push(` Sys ID: ${flow.sysID}`);
|
|
114
|
+
if (instanceURL && flow.sysID) {
|
|
115
|
+
lines.push(` Link: ${instanceURL}/sys_hub_flow.do?sys_id=${flow.sysID}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Subflow I/O section
|
|
119
|
+
if (flow.type && flow.type.toLowerCase() === 'subflow') {
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push('▶ SUBFLOW');
|
|
122
|
+
lines.push('─'.repeat(50));
|
|
123
|
+
|
|
124
|
+
if (inspection.flowInputs && inspection.flowInputs.length > 0) {
|
|
125
|
+
lines.push(` Inputs (${inspection.flowInputs.length})`);
|
|
126
|
+
for (const input of inspection.flowInputs) {
|
|
127
|
+
const name = firstNonEmpty(getStringField(input, 'label'), getStringField(input, 'name'), 'Input');
|
|
128
|
+
const typeName = getStringField(input, 'type');
|
|
129
|
+
if (typeName) {
|
|
130
|
+
lines.push(` • ${name}: ${typeName}`);
|
|
131
|
+
} else {
|
|
132
|
+
lines.push(` • ${name}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (inspection.flowOutputs && inspection.flowOutputs.length > 0) {
|
|
138
|
+
if (inspection.flowInputs && inspection.flowInputs.length > 0) {
|
|
139
|
+
lines.push('');
|
|
140
|
+
}
|
|
141
|
+
lines.push(` Outputs (${inspection.flowOutputs.length})`);
|
|
142
|
+
for (const out of inspection.flowOutputs) {
|
|
143
|
+
const name = firstNonEmpty(getStringField(out, 'label'), getStringField(out, 'name'), 'Output');
|
|
144
|
+
const typeName = getStringField(out, 'type');
|
|
145
|
+
if (typeName) {
|
|
146
|
+
lines.push(` • ${name}: ${typeName}`);
|
|
147
|
+
} else {
|
|
148
|
+
lines.push(` • ${name}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Trigger section
|
|
155
|
+
const { name: triggerName, type: triggerType, table: triggerTable, time: triggerTime, condition: triggerCondition } = extractTriggerDetails(inspection);
|
|
156
|
+
if (triggerName || triggerType || triggerTable || triggerTime || triggerCondition) {
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push('▶ TRIGGER');
|
|
159
|
+
lines.push('─'.repeat(50));
|
|
160
|
+
if (triggerName) lines.push(` Name: ${triggerName}`);
|
|
161
|
+
if (triggerType) lines.push(` Type: ${titleCase(triggerType.replace(/_/g, ' '))}`);
|
|
162
|
+
if (triggerTable) lines.push(` Table: ${triggerTable}`);
|
|
163
|
+
if (triggerTime) lines.push(` Time: ${triggerTime}`);
|
|
164
|
+
if (triggerCondition) lines.push(` Condition: ${formatTriggerCondition(triggerCondition)}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Flow structure section
|
|
168
|
+
lines.push('');
|
|
169
|
+
lines.push('⚡ FLOW STRUCTURE');
|
|
170
|
+
lines.push('─'.repeat(50));
|
|
171
|
+
lines.push(...formatFlowStructure(inspection));
|
|
172
|
+
|
|
173
|
+
lines.push('');
|
|
174
|
+
return lines.join('\n') + '\n';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function extractTriggerDetails(inspection) {
|
|
178
|
+
let name = '';
|
|
179
|
+
let type = '';
|
|
180
|
+
let table = '';
|
|
181
|
+
let time = '';
|
|
182
|
+
let condition = '';
|
|
183
|
+
|
|
184
|
+
if (inspection.version && typeof inspection.version === 'object') {
|
|
185
|
+
name = getStringField(inspection.version, 'trigger_name');
|
|
186
|
+
type = getStringField(inspection.version, 'trigger_type');
|
|
187
|
+
table = getStringField(inspection.version, 'trigger_table');
|
|
188
|
+
time = getStringField(inspection.version, 'trigger_time');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if ((!name || !type || !table || !time || !condition) && Object.keys(inspection.payload).length > 0) {
|
|
192
|
+
const triggers = inspection.payload.triggerInstances;
|
|
193
|
+
if (Array.isArray(triggers) && triggers.length > 0) {
|
|
194
|
+
const trigger = triggers[0];
|
|
195
|
+
if (trigger && typeof trigger === 'object') {
|
|
196
|
+
name = firstNonEmpty(name, getStringField(trigger, 'name'));
|
|
197
|
+
type = firstNonEmpty(type, getStringField(trigger, 'type'));
|
|
198
|
+
if (Array.isArray(trigger.inputs)) {
|
|
199
|
+
for (const input of trigger.inputs) {
|
|
200
|
+
if (!input || typeof input !== 'object') continue;
|
|
201
|
+
const k = getStringField(input, 'name');
|
|
202
|
+
const v = firstNonEmpty(getStringField(input, 'displayValue'), getStringField(input, 'value'));
|
|
203
|
+
if (k === 'table') table = firstNonEmpty(table, v);
|
|
204
|
+
if (k === 'time') time = firstNonEmpty(time, v);
|
|
205
|
+
if (k === 'condition') condition = firstNonEmpty(condition, v);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!name && inspection.triggerInstances && inspection.triggerInstances.length > 0) {
|
|
213
|
+
const first = inspection.triggerInstances[0];
|
|
214
|
+
name = firstNonEmpty(name, getStringField(first, 'name'), getStringField(first, 'display_text'), getNestedString(first, 'trigger_definition', 'display_value'));
|
|
215
|
+
type = firstNonEmpty(type, getStringField(first, 'trigger_type'), getNestedString(first, 'trigger_definition', 'display_value'));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (time.includes(' ')) {
|
|
219
|
+
const parts = time.split(' ');
|
|
220
|
+
if (parts.length === 2) {
|
|
221
|
+
time = parts[1];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { name, type, table, time, condition };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function inferFlowVersion(inspection) {
|
|
229
|
+
if (Object.keys(inspection.payload).length > 0) return 'Unset (Assumed V1)';
|
|
230
|
+
if (inspection.actionInstances && inspection.actionInstances.length > 0) return 'Unset (Assumed V1)';
|
|
231
|
+
return 'Unset';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatFlowStructure(inspection) {
|
|
235
|
+
const payload = inspection.payload;
|
|
236
|
+
if (Object.keys(payload).length > 0) {
|
|
237
|
+
return formatFlowStructureFromPayload(payload);
|
|
238
|
+
}
|
|
239
|
+
return formatFlowStructureFallback(inspection);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatFlowStructureFromPayload(payload) {
|
|
243
|
+
const childUIDs = new Set();
|
|
244
|
+
|
|
245
|
+
function markChildren(items) {
|
|
246
|
+
if (!Array.isArray(items)) return;
|
|
247
|
+
for (const item of items) {
|
|
248
|
+
if (!item || typeof item !== 'object') continue;
|
|
249
|
+
const uid = getStringField(item, 'uiUniqueIdentifier');
|
|
250
|
+
if (uid) childUIDs.add(uid);
|
|
251
|
+
if (Array.isArray(item.flowBlock)) {
|
|
252
|
+
markChildren(item.flowBlock);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (Array.isArray(payload.flowLogicInstances)) {
|
|
258
|
+
for (const logic of payload.flowLogicInstances) {
|
|
259
|
+
if (!logic || typeof logic !== 'object') continue;
|
|
260
|
+
if (Array.isArray(logic.flowBlock)) {
|
|
261
|
+
markChildren(logic.flowBlock);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const roots = [];
|
|
267
|
+
|
|
268
|
+
function addFromPayload(key, stepType) {
|
|
269
|
+
const items = payload[key];
|
|
270
|
+
if (!Array.isArray(items)) return;
|
|
271
|
+
for (const item of items) {
|
|
272
|
+
if (!item || typeof item !== 'object') continue;
|
|
273
|
+
const uid = getStringField(item, 'uiUniqueIdentifier');
|
|
274
|
+
if (uid && childUIDs.has(uid)) continue;
|
|
275
|
+
roots.push({ stepType, data: item, order: parseOrderField(item) });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
addFromPayload('actionInstances', 'action');
|
|
280
|
+
addFromPayload('subFlowInstances', 'subflow');
|
|
281
|
+
addFromPayload('flowLogicInstances', 'logic');
|
|
282
|
+
|
|
283
|
+
roots.sort((a, b) => a.order - b.order);
|
|
284
|
+
|
|
285
|
+
if (roots.length === 0) {
|
|
286
|
+
return [' (no steps found)'];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const lines = [];
|
|
290
|
+
let stepNum = 1;
|
|
291
|
+
|
|
292
|
+
function walk(steps, indent) {
|
|
293
|
+
for (const step of steps) {
|
|
294
|
+
const pad = ' '.repeat(indent);
|
|
295
|
+
lines.push(...formatStepLine(stepNum, pad, step));
|
|
296
|
+
stepNum++;
|
|
297
|
+
|
|
298
|
+
if (step.stepType !== 'logic') continue;
|
|
299
|
+
const block = step.data.flowBlock;
|
|
300
|
+
if (!Array.isArray(block) || block.length === 0) continue;
|
|
301
|
+
|
|
302
|
+
const children = [];
|
|
303
|
+
for (const raw of block) {
|
|
304
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
305
|
+
children.push({
|
|
306
|
+
stepType: classifyPayloadItem(raw),
|
|
307
|
+
data: raw,
|
|
308
|
+
order: parseOrderField(raw),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
children.sort((a, b) => a.order - b.order);
|
|
312
|
+
walk(children, indent + 1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
walk(roots, 0);
|
|
317
|
+
return lines;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatFlowStructureFallback(inspection) {
|
|
321
|
+
const steps = [];
|
|
322
|
+
|
|
323
|
+
if (inspection.actionInstances) {
|
|
324
|
+
for (const action of inspection.actionInstances) {
|
|
325
|
+
const name = firstNonEmpty(
|
|
326
|
+
getNestedString(action, 'action_type', 'display_value'),
|
|
327
|
+
getStringField(action, 'name'),
|
|
328
|
+
getStringField(action, 'display_text'),
|
|
329
|
+
'Action',
|
|
330
|
+
);
|
|
331
|
+
steps.push({ order: parseOrderField(action), text: name });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (inspection.flowLogicInstances) {
|
|
336
|
+
for (const logic of inspection.flowLogicInstances) {
|
|
337
|
+
const name = firstNonEmpty(
|
|
338
|
+
getNestedString(logic, 'logic_definition', 'display_value'),
|
|
339
|
+
getStringField(logic, 'name'),
|
|
340
|
+
getStringField(logic, 'display_text'),
|
|
341
|
+
'Logic',
|
|
342
|
+
);
|
|
343
|
+
steps.push({ order: parseOrderField(logic), text: name });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (inspection.subFlowInstances) {
|
|
348
|
+
for (const sf of inspection.subFlowInstances) {
|
|
349
|
+
const name = firstNonEmpty(
|
|
350
|
+
getNestedString(sf, 'subflow', 'display_value'),
|
|
351
|
+
getStringField(sf, 'name'),
|
|
352
|
+
getStringField(sf, 'display_text'),
|
|
353
|
+
'Subflow',
|
|
354
|
+
);
|
|
355
|
+
steps.push({ order: parseOrderField(sf), text: '↪ ' + name });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
steps.sort((a, b) => a.order - b.order);
|
|
360
|
+
|
|
361
|
+
if (steps.length === 0) {
|
|
362
|
+
return [' (no steps found)'];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return steps.map((step, i) => `${i + 1}. ${step.text}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function formatStepLine(stepNum, pad, step) {
|
|
369
|
+
switch (step.stepType) {
|
|
370
|
+
case 'logic':
|
|
371
|
+
return formatLogicStep(stepNum, pad, step.data);
|
|
372
|
+
case 'subflow':
|
|
373
|
+
return formatSubFlowStep(stepNum, pad, step.data);
|
|
374
|
+
default:
|
|
375
|
+
return formatActionStep(stepNum, pad, step.data);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function formatActionStep(stepNum, pad, action) {
|
|
380
|
+
const lines = [];
|
|
381
|
+
let actionName = firstNonEmpty(
|
|
382
|
+
getNestedString(action, 'actionType', 'fName'),
|
|
383
|
+
getStringField(action, 'actionName'),
|
|
384
|
+
getStringField(action, 'actionInternalName'),
|
|
385
|
+
getStringField(action, 'name'),
|
|
386
|
+
'Unknown Action',
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const idx = actionName.indexOf(' : ');
|
|
390
|
+
if (idx > 0) {
|
|
391
|
+
actionName = actionName.slice(idx + 3).trim();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let tableName = '';
|
|
395
|
+
if (Array.isArray(action.inputs)) {
|
|
396
|
+
for (const raw of action.inputs) {
|
|
397
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
398
|
+
if (getStringField(raw, 'name') === 'table_name') {
|
|
399
|
+
tableName = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let actionDisplay = actionName;
|
|
406
|
+
if (tableName && actionName === 'Update Record') {
|
|
407
|
+
actionDisplay = actionName + ' - ' + tableName;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const comment = firstNonEmpty(getStringField(action, 'comment'), getStringField(action, 'displayText'));
|
|
411
|
+
if (comment) {
|
|
412
|
+
lines.push(`${pad}${stepNum}. ${actionDisplay} (${comment})`);
|
|
413
|
+
} else {
|
|
414
|
+
lines.push(`${pad}${stepNum}. ${actionDisplay}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (Array.isArray(action.inputs)) {
|
|
418
|
+
for (const raw of action.inputs) {
|
|
419
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
420
|
+
const inputName = getStringField(raw, 'name');
|
|
421
|
+
if (inputName === 'table_name') continue;
|
|
422
|
+
|
|
423
|
+
let inputValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
424
|
+
if (!inputValue) continue;
|
|
425
|
+
if (inputValue.length > 50) {
|
|
426
|
+
inputValue = inputValue.slice(0, 47) + '...';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let label = inputName;
|
|
430
|
+
if (raw.parameter && typeof raw.parameter === 'object') {
|
|
431
|
+
label = firstNonEmpty(getStringField(raw.parameter, 'label'), label);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
lines.push(`${pad} ${label}: ${inputValue}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return lines;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function formatSubFlowStep(stepNum, pad, subFlow) {
|
|
442
|
+
const lines = [];
|
|
443
|
+
const subFlowName = firstNonEmpty(
|
|
444
|
+
getNestedString(subFlow, 'subFlowType', 'fName'),
|
|
445
|
+
getStringField(subFlow, 'subFlowName'),
|
|
446
|
+
getStringField(subFlow, 'subFlowInternalName'),
|
|
447
|
+
getNestedString(subFlow, 'subFlow', 'name'),
|
|
448
|
+
getStringField(subFlow, 'name'),
|
|
449
|
+
'Unknown Subflow',
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const comment = getStringField(subFlow, 'comment');
|
|
453
|
+
if (comment) {
|
|
454
|
+
lines.push(`${pad}${stepNum}. ↪ ${subFlowName} (${comment})`);
|
|
455
|
+
} else {
|
|
456
|
+
lines.push(`${pad}${stepNum}. ↪ ${subFlowName}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
lines.push(`${pad} jsn flows "${subFlowName}"`);
|
|
460
|
+
return lines;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function formatLogicStep(stepNum, pad, logic) {
|
|
464
|
+
const lines = [];
|
|
465
|
+
const logicType = firstNonEmpty(getNestedString(logic, 'flowLogicDefinition', 'name'), getStringField(logic, 'name'), 'Logic Step');
|
|
466
|
+
|
|
467
|
+
const comment = getStringField(logic, 'comment');
|
|
468
|
+
let condition = '';
|
|
469
|
+
let conditionLabel = '';
|
|
470
|
+
|
|
471
|
+
if (logicType === 'If' || logicType === 'Else If') {
|
|
472
|
+
if (Array.isArray(logic.inputs)) {
|
|
473
|
+
for (const raw of logic.inputs) {
|
|
474
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
475
|
+
const inputName = getStringField(raw, 'name');
|
|
476
|
+
if (inputName === 'condition') {
|
|
477
|
+
condition = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
478
|
+
}
|
|
479
|
+
if (inputName === 'condition_name') {
|
|
480
|
+
conditionLabel = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let displayText = logicType;
|
|
487
|
+
if (conditionLabel) {
|
|
488
|
+
displayText = logicType + ': ' + conditionLabel;
|
|
489
|
+
} else if (condition && condition.length < 60) {
|
|
490
|
+
displayText = logicType + ': ' + condition;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
lines.push(`${pad}${stepNum}. ${displayText}`);
|
|
494
|
+
|
|
495
|
+
if (condition && condition.length >= 60 && !conditionLabel) {
|
|
496
|
+
lines.push(`${pad} Condition: ${condition}`);
|
|
497
|
+
}
|
|
498
|
+
if (comment) {
|
|
499
|
+
lines.push(`${pad} Annotation: ${comment}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (logicType === 'Set Flow Variables') {
|
|
503
|
+
if (Array.isArray(logic.flowVariables) && logic.flowVariables.length > 0) {
|
|
504
|
+
lines.push(`${pad} Variables Set:`);
|
|
505
|
+
for (const raw of logic.flowVariables) {
|
|
506
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
507
|
+
const varName = getStringField(raw, 'name');
|
|
508
|
+
const varValue = firstNonEmpty(getStringField(raw, 'displayValue'), getStringField(raw, 'value'));
|
|
509
|
+
if (!varName) continue;
|
|
510
|
+
if (varValue) {
|
|
511
|
+
lines.push(`${pad} • ${varName} = ${varValue}`);
|
|
512
|
+
} else {
|
|
513
|
+
lines.push(`${pad} • ${varName}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return lines;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function classifyPayloadItem(m) {
|
|
523
|
+
if (m.flowLogicDefinition) return 'logic';
|
|
524
|
+
if (m.subFlowType) return 'subflow';
|
|
525
|
+
if (m.subflowSysId) return 'subflow';
|
|
526
|
+
if (m.subFlow) return 'subflow';
|
|
527
|
+
return 'action';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function getNestedString(record, parent, field) {
|
|
531
|
+
const node = record?.[parent];
|
|
532
|
+
if (!node || typeof node !== 'object') return '';
|
|
533
|
+
return getStringField(node, field);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function firstNonEmpty(...values) {
|
|
537
|
+
for (const v of values) {
|
|
538
|
+
if (v && String(v).trim() !== '') return String(v).trim();
|
|
539
|
+
}
|
|
540
|
+
return '';
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function titleCase(s) {
|
|
544
|
+
return s
|
|
545
|
+
.toLowerCase()
|
|
546
|
+
.split(/\s+/)
|
|
547
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
548
|
+
.join(' ');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function formatTriggerCondition(condition) {
|
|
552
|
+
if (!condition) return '';
|
|
553
|
+
let result = condition;
|
|
554
|
+
result = result.replace(/\^OR/g, ' OR ');
|
|
555
|
+
result = result.replace(/\^/g, ' AND ');
|
|
556
|
+
result = result.replace(/!=/g, ' != ');
|
|
557
|
+
result = result.replace(/>=/g, ' >= ');
|
|
558
|
+
result = result.replace(/<=/g, ' <= ');
|
|
559
|
+
result = result.replace(/=/g, ' = ');
|
|
560
|
+
result = result.replace(/>/g, ' > ');
|
|
561
|
+
result = result.replace(/</g, ' < ');
|
|
562
|
+
result = result.replace(/LIKE/g, ' LIKE ');
|
|
563
|
+
while (result.includes(' ')) {
|
|
564
|
+
result = result.replace(/ {2}/g, ' ');
|
|
565
|
+
}
|
|
566
|
+
return result.trim();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function parseOrderField(record) {
|
|
570
|
+
const order = getStringField(record, 'order');
|
|
571
|
+
const n = parseInt(order, 10);
|
|
572
|
+
return isNaN(n) ? 0 : n;
|
|
573
|
+
}
|