@jacebenson/jsn 0.0.3

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