@jacebenson/jsn 4.3.13-dev.87.1 → 4.3.13-dev.88.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.13-dev.87.1",
3
+ "version": "4.3.13-dev.88.1",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ import { constrainQuery, constrainedQueryHelp } from '../constrained-query.js';
1
2
  import { enrichRelatedLists, relatedListOptions } from '../related-lists.js';
2
3
  // Generic table-based CRUD command builder
3
4
 
@@ -122,19 +123,24 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
122
123
  command: 'list',
123
124
  aliases: ['ls'],
124
125
  describe: `List ${name}`,
125
- builder: (y) => recordOptions(y)
126
- .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true^priority=1")' })
127
- .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
128
- .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
126
+ builder: (y) => {
127
+ const command = recordOptions(y)
128
+ .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true^priority=1")' })
129
+ .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
130
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' });
131
+ const help = [opts.help, extraQuery ? constrainedQueryHelp(extraQuery) : ''].filter(Boolean).join('\n');
132
+ return help ? command.epilogue(help) : command;
133
+ },
129
134
  handler: wrap(async (argv, app) => {
130
135
  const query = argv.query || '';
136
+ const constrainedQuery = constrainQuery(query, extraQuery);
131
137
  const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
132
138
  const limit = argv.limit;
133
139
 
134
140
  // Interactive picker with pagination
135
141
  if (canPrompt() && app.output.getFormat() === FormatAuto && !query) {
136
142
  const picked = await interactiveList({
137
- app, table, singular, columns: ['name', 'sys_scope'], limit, query: extraQuery,
143
+ app, table, singular, columns: ['name', 'sys_scope'], limit, query: constrainedQuery,
138
144
  labelField: 'name',
139
145
  message: `Select ${vowelArticle(singular)} ${singular}`,
140
146
  formatLabel: (r) => {
@@ -162,8 +168,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
162
168
  }
163
169
 
164
170
  // Non-interactive: text/table output
165
- const baseQ = extraQuery ? extraQuery + '^' : '';
166
- const q = query ? query + '^ORDERBYDESCsys_updated_on' : (baseQ + 'ORDERBYDESCsys_updated_on');
171
+ const q = `${constrainedQuery}${constrainedQuery ? '^' : ''}ORDERBYDESCsys_updated_on`;
167
172
  const params = recordListParams({ limit, fields: ['sys_id', ...columns], query: q });
168
173
  const records = await listRecords(app.sdk, { table, params, displayValues: argv.displayValues ?? opts.referenceMode });
169
174
  app.ok({
@@ -277,13 +282,14 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
277
282
  });
278
283
  }
279
284
 
280
- return y;
285
+ const help = [opts.help, extraQuery ? constrainedQueryHelp(extraQuery) : ''].filter(Boolean).join('\n');
286
+ return help ? y.epilogue(help) : y;
281
287
  };
282
288
 
283
289
  return {
284
290
  command: `${name} [subcommand]`,
285
291
  aliases: aliases || [],
286
- describe: `Manage ${name} (e.g. "${name} list --query nameLIKEincident")`,
292
+ describe: opts.describe || `Manage ${name} (e.g. "${name} list --query nameLIKEincident")`,
287
293
  discovery: {
288
294
  table,
289
295
  defaultColumns,
@@ -1,5 +1,10 @@
1
1
  import { buildDevCmd } from './_generic.js';
2
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: 'when=before' });
3
+ // Query rules are explicitly marked; their order is not a classification rule.
4
+ export const b4rulesCmd = wrap => buildDevCmd('b4rules', 'sys_script', ['b4rule'],
5
+ ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, {
6
+ singular: 'before-query business rule', scopeValidation: true,
7
+ extraQuery: 'when=before^action_query=true',
8
+ describe: 'Inspect before-query business rules that can filter record reads',
9
+ help: 'Before-query business rules use when=before and the Boolean action_query=true. Filter collection and active status for the affected table. For field behavior, see jsn columns --help; do not bypass server rules or ACLs.',
10
+ });
@@ -1,9 +1,10 @@
1
+ import { fieldBehaviorHelp } from '../field-behavior.js';
1
2
  import { buildDevCmd } from './_generic.js';
2
3
 
3
4
  export const columnsCmd = (wrap) => {
4
5
  const base = buildDevCmd('columns', 'sys_dictionary', ['column', 'col'],
5
6
  ['element', 'column_label', 'internal_type', 'mandatory', 'max_length', 'active'],
6
- wrap, { singular: 'column', scopeValidation: true });
7
+ wrap, { singular: 'column', scopeValidation: true, help: fieldBehaviorHelp });
7
8
 
8
9
  // Add --table filter
9
10
  const origBuilder = base.builder;
@@ -0,0 +1,20 @@
1
+ import { errUsageHint } from './errors.js';
2
+
3
+ /** Constrained table views accept literal AND filters. Encoded queries have no
4
+ * portable parenthesization; OR/NQ/dynamic clauses must not escape fixed filters. */
5
+ export function constrainQuery(query = '', constraint = '') {
6
+ if (!constraint) return query;
7
+ const clauses = String(query).split('^').filter((clause, index, values) => clause !== 'EQ' || index !== values.length - 1);
8
+ if (!query) return constraint;
9
+ const literal = /^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*(?:(?:!=|>=|<=|=|>|<|NOT LIKE|STARTSWITH|ENDSWITH|LIKE|NOT IN|IN)[^^]*|ISEMPTY|ISNOTEMPTY)$/;
10
+ const ordering = /^ORDERBY(?:DESC)?[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*$/;
11
+ const allowed = clause => ordering.test(clause) || (literal.test(clause)
12
+ && (!/^(?:NQ|OR)/i.test(clause) || /^order(?:!=|>=|<=|=|>|<|NOT LIKE|STARTSWITH|ENDSWITH|LIKE|NOT IN|IN|ISEMPTY|ISNOTEMPTY)/.test(clause)));
13
+ if (/javascript\s*:/i.test(query) || clauses.some(clause => !allowed(clause))) {
14
+ throw errUsageHint('This constrained command accepts literal AND filters and ORDERBY only; OR, NQ and dynamic query clauses are unsupported.',
15
+ `The fixed filter is ${constraint}. Use literal AND clauses, or explicitly use records list for advanced queries and retain the intended constraints in every branch.`);
16
+ }
17
+ return clauses.length ? `${constraint}^${clauses.join('^')}` : constraint;
18
+ }
19
+
20
+ export const constrainedQueryHelp = constraint => `Always includes: ${constraint}. Additional --query filters use literal AND clauses (plus ORDERBY). OR, NQ and dynamic query clauses are rejected before requests so they cannot bypass this view's fixed filter.`;
@@ -0,0 +1,15 @@
1
+ // Schema-backed investigation paths; no universal priority order is assumed.
2
+ export const fieldBehaviorHelp = [
3
+ 'Investigate the observed symptom; there is no fixed priority order for every field problem.',
4
+ 'Query filtering: jsn b4rules list --query "collection=incident^active=true" --json',
5
+ 'Before-query rules use sys_script.when=before and the Boolean action_query=true.',
6
+ 'Dictionary entry: jsn records list --table sys_dictionary --query "name=incident^element=caller_id" --json',
7
+ 'Inherited dictionary overrides: jsn records list --table sys_dictionary_override --query "name=incident^element=caller_id" --json',
8
+ 'Inspect base_table and inherited tables as needed; the dictionary entry is separate from its overrides.',
9
+ 'Access controls: jsn acls list --query "nameINincident,incident.*,incident.caller_id,*.caller_id,*,*.*" --json',
10
+ 'Also inspect inherited-table and wildcard ACLs. Exact name=incident alone omits field/wildcard rules.',
11
+ 'ACL rows are evidence, not proof of effective access: operation, roles, conditions and scripts matter.',
12
+ 'Form behavior: jsn clientscripts list --query "table=incident^active=true" --json',
13
+ 'Form policies: jsn uipolicies list --query "table=incident^active=true" --json',
14
+ 'Use the command help and show output to inspect the relevant records. Never bypass a business rule, ACL or other protection to make a symptom disappear.',
15
+ ].join('\n');
@@ -18,7 +18,7 @@ export default {
18
18
  appmenu: { table: 'sys_app_application', aliases: ['app-menu', 'menu'], columns: ['name', 'active', 'sys_scope'],
19
19
  options: { singular: 'application menu', scopeValidation: true } },
20
20
  acls: { table: 'sys_security_acl', aliases: ['acl'], columns: ['name', 'operation', 'type', 'active', 'sys_scope'],
21
- options: { singular: 'ACL', scopeValidation: true } },
21
+ options: { singular: 'ACL', scopeValidation: true, help: 'Investigate table, field, wildcard and inherited ACL names for the relevant operation. Exact name=incident alone does not cover incident.caller_id, incident.*, *.caller_id or inherited-table ACLs. Roles, conditions and scripts affect effective access; rows alone do not prove access. See jsn columns --help for field investigation paths.' } },
22
22
  roles: { table: 'sys_user_role', aliases: ['role', 'r'], columns: ['name', 'description', 'elevated_privilege', 'sys_scope'],
23
23
  options: { singular: 'role', scopeValidation: true } },
24
24
  properties: { table: 'sys_properties', aliases: ['property', 'prop'], columns: ['name', 'value', 'description', 'sys_scope'],