@jacebenson/jsn 3.0.6 → 3.1.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.
@@ -2,8 +2,7 @@
2
2
 
3
3
  import { formatRecordForDisplay, getStringField, isHexString, parseDataArg } from '../../helpers.js';
4
4
  import { getCurrentUser, getCurrentApplication } from '../../context.js';
5
- import readline from 'node:readline';
6
- import search from '@inquirer/search';
5
+ import { paginatedSearch } from '../../paginated-search.js';
7
6
  import { isTTY, FormatAuto } from '../../output.js';
8
7
 
9
8
  function vowelArticle(word) {
@@ -16,13 +15,13 @@ function buildHints(name, singular, readOnly) {
16
15
  if (!readOnly) {
17
16
  crumbs.push({
18
17
  action: 'create',
19
- cmd: `jsn dev ${name} create --name ... --label "..."`,
18
+ cmd: `jsn ${name} create --name ... --label "..."`,
20
19
  description: `Create ${vowelArticle(singular)} ${singular}`,
21
20
  });
22
21
  }
23
22
  crumbs.push({
24
23
  action: 'show',
25
- cmd: `jsn dev ${name} show <name_or_sys_id>`,
24
+ cmd: `jsn ${name} show <name_or_sys_id>`,
26
25
  description: `Show ${singular} details`,
27
26
  });
28
27
  return crumbs;
@@ -81,15 +80,16 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
81
80
  const singular = toSingular(name, opts.singular);
82
81
  const readOnly = opts.readOnly || false;
83
82
  const scopeValidation = opts.scopeValidation || false;
83
+ const extraQuery = opts.extraQuery || '';
84
84
  const showSummary = opts.showSummary || ((record, id) => `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${getStringField(record, 'name') || id}`);
85
85
  const showBreadcrumbs = opts.showBreadcrumbs || ((record, id) => {
86
86
  const crumbs = [
87
- { action: 'list', cmd: `jsn dev ${name} list`, description: `Back to all ${name}` },
87
+ { action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
88
88
  ];
89
89
  if (!readOnly) {
90
90
  crumbs.unshift(
91
- { action: 'delete', cmd: `jsn dev ${name} delete ${id}`, description: `Delete this ${singular}` },
92
- { action: 'update', cmd: `jsn dev ${name} update ${id} --data '{...}'`, description: `Update this ${singular}` }
91
+ { action: 'delete', cmd: `jsn ${name} delete ${id}`, description: `Delete this ${singular}` },
92
+ { action: 'update', cmd: `jsn ${name} update ${id} --data '{...}'`, description: `Update this ${singular}` }
93
93
  );
94
94
  }
95
95
  return crumbs;
@@ -104,71 +104,58 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
104
104
  builder: (y) => y
105
105
  .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true^priority=1")' })
106
106
  .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
107
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
107
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
108
108
  handler: wrap(async (argv, app) => {
109
109
  const query = argv.query || '';
110
110
  const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
111
111
  const limit = argv.limit;
112
112
 
113
- // Interactive picker in TTY with auto format and no explicit query
113
+ // Interactive picker with pagination
114
114
  const interactive = isTTY(process.stdout) && isTTY(process.stdin);
115
115
  if (interactive && app.output.getFormat() === FormatAuto && !query) {
116
- const pickerColumns = ['sys_id', 'name', ...columns.filter(c => c !== 'name' && c !== 'sys_id')];
117
- const pickerParams = new URLSearchParams();
118
- pickerParams.set('sysparm_limit', String(limit));
119
- pickerParams.set('sysparm_display_value', 'all');
120
- pickerParams.set('sysparm_fields', pickerColumns.join(','));
121
- pickerParams.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
116
+ // Get total count
117
+ let totalCount = 0;
118
+ try {
119
+ totalCount = await app.sdk.aggregateCount(table, extraQuery || '');
120
+ } catch { /* non-critical */ }
122
121
 
123
- const pickerRecords = await app.sdk.list(table, pickerParams);
124
- if (pickerRecords.length === 0) {
125
- app.ok({
126
- table,
127
- count: 0,
128
- columns: pickerColumns,
129
- records: [],
130
- pagination: { limit, offset: 0 },
131
- context: { instance_url: app.getEffectiveInstance() },
132
- }, {
133
- summary: `0 ${name}(s)`,
134
- breadcrumbs: buildHints(name, singular, readOnly),
122
+ const source = async (term, offset, { signal } = {}) => {
123
+ const params = new URLSearchParams();
124
+ params.set('sysparm_limit', String(limit));
125
+ params.set('sysparm_display_value', 'all');
126
+ params.set('sysparm_fields', ['sys_id', 'name', 'sys_scope'].join(','));
127
+ if (term) {
128
+ params.set('sysparm_query', `nameLIKE${term}^ORDERBYDESCsys_updated_on`);
129
+ } else {
130
+ params.set('sysparm_query', (extraQuery ? extraQuery + '^' : '') + 'ORDERBYDESCsys_updated_on');
131
+ params.set('sysparm_offset', String(offset));
132
+ }
133
+ const records = await app.sdk.list(table, params);
134
+ return records.map(r => {
135
+ const recordName = getStringField(r, 'name') || getStringField(r, 'sys_id');
136
+ const scope = getStringField(r, 'sys_scope') || '';
137
+ let label = recordName;
138
+ if (scope && scope !== 'global') label += ` [${scope}]`;
139
+ return { name: label, value: recordName };
135
140
  });
136
- return;
137
- }
141
+ };
138
142
 
139
- const choices = pickerRecords.map(r => {
140
- const recordName = getStringField(r, 'name') || getStringField(r, 'sys_id');
141
- const scope = getStringField(r, 'sys_scope') || '';
142
- let label = recordName;
143
- if (scope && scope !== 'global') label += ` [${scope}]`;
144
- return { name: label, value: recordName };
143
+ const selectedName = await paginatedSearch({
144
+ message: `Select ${vowelArticle(singular)} ${singular}`,
145
+ pageSize: 10,
146
+ totalCount,
147
+ source,
145
148
  });
146
149
 
147
- let selectedName;
148
- try {
149
- selectedName = await search({
150
- message: `Select ${vowelArticle(singular)} ${singular}:`,
151
- source: async (input) => {
152
- if (!input) return choices;
153
- const term = input.toLowerCase();
154
- return choices.filter(c => c.name.toLowerCase().includes(term));
155
- },
156
- });
157
- } catch (err) {
158
- if (err.name === 'ExitPromptError' || (err.message && err.message.includes('force closed'))) {
159
- return;
160
- }
161
- throw err;
162
- }
163
-
164
150
  if (selectedName) {
151
+ const recordName = typeof selectedName === 'object' ? selectedName.value : selectedName;
165
152
  const showParams = new URLSearchParams();
166
- showParams.set('sysparm_query', `name=${selectedName}`);
153
+ showParams.set('sysparm_query', `name=${recordName}`);
167
154
  showParams.set('sysparm_display_value', 'all');
168
155
  showParams.set('sysparm_limit', '1');
169
156
  const showRecords = await app.sdk.list(table, showParams);
170
157
  if (showRecords.length === 0) {
171
- throw new Error(`${singular} not found: ${selectedName}`);
158
+ throw new Error(`${singular} not found: ${recordName}`);
172
159
  }
173
160
  const record = showRecords[0];
174
161
  record._context = { instance_url: app.getEffectiveInstance(), table };
@@ -176,10 +163,10 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
176
163
  await opts.onShow(record, app);
177
164
  }
178
165
  app.ok(record, {
179
- summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${selectedName}`,
166
+ summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${recordName}`,
180
167
  breadcrumbs: [
181
- ...(readOnly ? [] : [{ action: 'update', cmd: `jsn dev ${name} update ${selectedName} --data '{...}'`, description: `Update this ${singular}` }]),
182
- { action: 'list', cmd: `jsn dev ${name} list`, description: `Back to all ${name}` },
168
+ ...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${recordName} --data '{...}'`, description: `Update this ${singular}` }]),
169
+ { action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
183
170
  ],
184
171
  });
185
172
  }
@@ -191,7 +178,8 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
191
178
  params.set('sysparm_limit', String(limit));
192
179
  params.set('sysparm_display_value', 'all');
193
180
  params.set('sysparm_fields', ['sys_id', ...columns].join(','));
194
- const q = query ? query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
181
+ const baseQ = extraQuery ? extraQuery + '^' : '';
182
+ const q = query ? query + '^ORDERBYDESCsys_updated_on' : (baseQ + 'ORDERBYDESCsys_updated_on');
195
183
  params.set('sysparm_query', q);
196
184
  const records = await app.sdk.list(table, params);
197
185
  app.ok({
@@ -256,7 +244,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
256
244
  app.ok(record, {
257
245
  summary: `Created ${singular}`,
258
246
  breadcrumbs: [
259
- { action: 'show', cmd: `jsn dev ${name} show ${getStringField(record, 'name') || getStringField(record, 'sys_id')}`, description: `View the new ${singular}` },
247
+ { action: 'show', cmd: `jsn ${name} show ${getStringField(record, 'name') || getStringField(record, 'sys_id')}`, description: `View the new ${singular}` },
260
248
  ],
261
249
  });
262
250
  }),
@@ -284,7 +272,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
284
272
  if (scopeValidation) {
285
273
  const scopeErr = await checkScope(app.sdk, recordScope);
286
274
  if (scopeErr) {
287
- throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn dev scopes set ${scopeErr.recordScope}`);
275
+ throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
288
276
  }
289
277
  }
290
278
 
@@ -315,7 +303,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
315
303
  if (scopeValidation) {
316
304
  const scopeErr = await checkScope(app.sdk, recordScope);
317
305
  if (scopeErr) {
318
- throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn dev scopes set ${scopeErr.recordScope}`);
306
+ throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
319
307
  }
320
308
  }
321
309
 
@@ -358,7 +346,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
358
346
  console.log(' update Update a record');
359
347
  console.log(' delete Delete a record');
360
348
  }
361
- console.log(`\nRun "jsn dev ${name} <command> --help" for details.`);
349
+ console.log(`\nRun "jsn ${name} <command> --help" for details.`);
362
350
  },
363
351
  };
364
352
  }
@@ -97,4 +97,31 @@ export const securitytypesCmd = (wrap) => buildDevCmd('securitytypes', 'sys_secu
97
97
  export const uxscriptsCmd = (wrap) => buildDevCmd('uxscripts', 'sys_ux_lib_source_script', ['uxscript', 'ux'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UX script', scopeValidation: true });
98
98
  export const aliasesCmd = (wrap) => buildDevCmd('aliases', 'sys_alias', ['alias', 'als'], ['name', 'table', 'sys_scope'], wrap, { singular: 'alias', scopeValidation: true });
99
99
  export const catalogscriptsCmd = (wrap) => buildDevCmd('catalogscripts', 'catalog_script_client', ['catalogscript', 'cs'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog script', scopeValidation: true });
100
+
101
+ // ── Automation: Async ──
102
+ export const scriptactionsCmd = (wrap) => buildDevCmd('scriptactions', 'sysevent_script_action', ['scriptaction'], ['name', 'active', 'sys_scope'], wrap, { singular: 'script action', scopeValidation: true });
103
+ export const scheduledjobsCmd = (wrap) => buildDevCmd('scheduledjobs', 'sysauto_script', ['scheduledjob'], ['name', 'active', 'run_type', 'sys_scope'], wrap, { singular: 'scheduled job', scopeValidation: true });
104
+ export const asyncrulesCmd = (wrap) => buildDevCmd('asyncrules', 'sys_script', ['asyncrule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'async business rule', scopeValidation: true, extraQuery: 'order>=1000' });
105
+ export const triggersCmd = (wrap) => buildDevCmd('triggers', 'sysevent_register', ['trigger'], ['name', 'event_name', 'active', 'sys_scope'], wrap, { singular: 'event trigger', scopeValidation: true });
106
+
107
+ // ── Automation: In Memory ──
108
+ export const workflowsCmd = (wrap) => buildDevCmd('workflows', 'wf_workflow', ['workflow'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'workflow', scopeValidation: true, readOnly: true });
109
+ export const decisiontablesCmd = (wrap) => buildDevCmd('decisiontables', 'sys_decision', ['decisiontable'], ['name', 'active', 'sys_scope'], wrap, { singular: 'decision table', scopeValidation: true, readOnly: true });
110
+ export const assignmentsCmd = (wrap) => buildDevCmd('assignments', 'sysrule_assignment', ['assignment'], ['name', 'active', 'sys_scope'], wrap, { singular: 'assignment rule', scopeValidation: true, readOnly: true });
111
+
112
+ // ── Automation: Inbound ──
113
+ export const emailCmd = (wrap) => buildDevCmd('email', 'sysevent_email_action', ['emails'], ['name', 'active', 'event_name', 'sys_scope'], wrap, { singular: 'inbound email action', scopeValidation: true });
114
+
115
+ // ── Automation: Outbound ──
116
+ export const restmessageCmd = (wrap) => buildDevCmd('restmessage', 'sys_rest_message', ['rm', 'restmsg'], ['name', 'endpoint', 'active', 'sys_scope'], wrap, { singular: 'REST message', scopeValidation: true });
117
+ export const restmethodsCmd = (wrap) => buildDevCmd('restmethods', 'sys_rest_message_fn', ['restmethod'], ['name', 'http_method', 'active', 'sys_scope'], wrap, { singular: 'REST method', scopeValidation: true });
118
+ export const soapmessagesCmd = (wrap) => buildDevCmd('soapmessages', 'sys_soap_message', ['soapmsg'], ['name', 'active', 'sys_scope'], wrap, { singular: 'SOAP message', scopeValidation: true });
119
+ export const soapfunctionsCmd = (wrap) => buildDevCmd('soapfunctions', 'sys_soap_message_function', ['soapfunc'], ['name', 'active', 'sys_scope'], wrap, { singular: 'SOAP function', scopeValidation: true });
120
+
121
+ // ── UX: Core UI ──
122
+ export const uimacrosCmd = (wrap) => buildDevCmd('uimacros', 'sys_ui_macro', ['uimacro'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UI macro', scopeValidation: true });
123
+
124
+ // ── UX: Workspaces ──
125
+ export const uxlistsCmd = (wrap) => buildDevCmd('uxlists', 'sys_ux_list', ['uxlist'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace list', scopeValidation: true, readOnly: true });
126
+ export const uxapplicabilityCmd = (wrap) => buildDevCmd('uxapplicability', 'sys_ux_applicability_m2m', ['uxapp'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace applicability', scopeValidation: true, readOnly: true });
100
127
  export const cataloguipoliciesCmd = (wrap) => buildDevCmd('cataloguipolicies', 'catalog_ui_policy', ['cataloguipolicy', 'cup'], ['short_description', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog UI policy', scopeValidation: true });
@@ -0,0 +1,5 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+
3
+ // Before-query business rules — applies filters to GlideRecord queries before they execute.
4
+ // These are sys_script records with order < 100.
5
+ export const b4rulesCmd = (wrap) => buildDevCmd('b4rules', 'sys_script', ['b4rule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'before-query business rule', scopeValidation: true, extraQuery: 'order<100' });
@@ -45,7 +45,7 @@ export function evalCmd(wrap) {
45
45
  summary: 'Script executed' + warning,
46
46
  breadcrumbs: [{
47
47
  action: 'eval',
48
- cmd: 'jsn dev eval --script \'...\'',
48
+ cmd: 'jsn eval --script \'...\'',
49
49
  description: 'Execute a background script on the ServiceNow instance',
50
50
  }],
51
51
  });
@@ -14,22 +14,24 @@ 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: 20, describe: 'Max records' }),
17
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
18
18
  handler: wrap(async (argv, app) => {
19
+ app.requireInstance();
19
20
  const columns = argv.columns ? argv.columns.split(',') : ['name', 'active', 'description', 'sys_created_by', 'sys_updated_on'];
20
21
  const query = argv.query || '';
21
22
 
22
23
  // Interactive picker
23
24
  const picked = await interactiveList({
24
25
  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
+ formatLabel: r => `${getStringField(r, 'active') === 'true' ? '🟢' : '🔴'} ${getStringField(r, 'name')}`,
26
27
  });
28
+ if (picked === undefined) return; // user cancelled
27
29
  if (picked) {
28
30
  const inspection = await app.sdk.inspectFlow(getStringField(picked, 'sys_id'));
29
31
  const formatted = formatFlowInspection(inspection, app.getEffectiveInstance());
30
32
  return app.ok({ ...inspection, _formatted: formatted }, {
31
33
  summary: `Flow: ${inspection.flow.name}`,
32
- breadcrumbs: [{ action: 'list', cmd: 'jsn dev flows list', description: 'Back to all flows' }],
34
+ breadcrumbs: [{ action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' }],
33
35
  });
34
36
  }
35
37
 
@@ -61,7 +63,7 @@ export function flowsCmd(wrap) {
61
63
  app.ok(data, {
62
64
  summary: `Flow: ${inspection.flow.name}`,
63
65
  breadcrumbs: [
64
- { action: 'list', cmd: 'jsn dev flows list', description: 'Back to all flows' },
66
+ { action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' },
65
67
  ],
66
68
  });
67
69
  }),
@@ -73,7 +75,7 @@ export function flowsCmd(wrap) {
73
75
  .option('data', { type: 'string', describe: 'JSON data for the flow' }),
74
76
  handler: wrap(async (_argv, _app) => {
75
77
  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.');
78
+ + 'Use the ServiceNow web UI to create flows, then use "jsn flows list" to view them.');
77
79
  }),
78
80
  })
79
81
  .command({
@@ -83,7 +85,7 @@ export function flowsCmd(wrap) {
83
85
  .option('data', { type: 'string', describe: 'JSON data to update' }),
84
86
  handler: wrap(async (_argv, _app) => {
85
87
  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.');
88
+ + 'Use the ServiceNow web UI to update flows, then use "jsn flows list" to view them.');
87
89
  }),
88
90
  })
89
91
  .command({
@@ -95,7 +97,22 @@ export function flowsCmd(wrap) {
95
97
  }),
96
98
  });
97
99
  },
98
- handler: () => {},
100
+ handler: () => {
101
+ console.log('Manage Flow Designer flows from the sys_hub_flow table.');
102
+ console.log('');
103
+ console.log('Available subcommands:');
104
+ console.log(' list List flows');
105
+ console.log(' show <identifier> Show flow details by name or sys_id');
106
+ console.log(' create Create a new flow (not yet implemented)');
107
+ console.log(' update <identifier> Update a flow (not yet implemented)');
108
+ console.log(' delete <identifier> Delete a flow (not yet implemented)');
109
+ console.log('');
110
+ console.log('Run "jsn flows <command> --help" for details.');
111
+ console.log('');
112
+ console.log('Note: Flow inspection detail varies. V2 flows (with payload)');
113
+ console.log('show full action inputs and conditions. Subflows and V1 flows');
114
+ console.log('show step names only.');
115
+ },
99
116
  };
100
117
  }
101
118
 
@@ -168,7 +185,13 @@ function formatFlowInspection(inspection, instanceURL) {
168
185
  lines.push('');
169
186
  lines.push('⚡ FLOW STRUCTURE');
170
187
  lines.push('─'.repeat(50));
171
- lines.push(...formatFlowStructure(inspection));
188
+ const structureLines = formatFlowStructure(inspection);
189
+ lines.push(...structureLines);
190
+ // Note limitation for V1/subflows without payload detail
191
+ if (!inspection.payload || Object.keys(inspection.payload).length === 0) {
192
+ lines.push('');
193
+ lines.push(' (step detail limited — this flow lacks a V2 payload)');
194
+ }
172
195
 
173
196
  lines.push('');
174
197
  return lines.join('\n') + '\n';