@jacebenson/jsn 4.0.2 → 4.0.4
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 +29 -7
- package/package.json +3 -4
- package/src/app.js +25 -3
- package/src/cli.js +3 -1
- package/src/commands/auth.js +222 -46
- package/src/commands/dev/domains.js +292 -0
- package/src/commands/dev/scopes.js +88 -36
- package/src/commands/dev/updatesets.js +297 -106
- package/src/commands/dev.js +16 -1
- package/src/commands/records.js +4 -3
- package/src/commands/setup.js +53 -17
- package/src/context.js +52 -5
- package/src/help.js +18 -3
- package/src/mutations.js +10 -1
- package/src/sdk.js +4 -0
- package/suppress-warnings.cjs +0 -13
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Domain separation commands — list/show/current/set
|
|
2
|
+
//
|
|
3
|
+
// Domain separation (plugin) adds a `domain` table holding the domain
|
|
4
|
+
// hierarchy. Each domain-separated table's rows carry `sys_domain` +
|
|
5
|
+
// `sys_domain_path`, and queries are auto-filtered to the session domain
|
|
6
|
+
// plus its children. There is NO durable platform-side "current domain"
|
|
7
|
+
// preference a CLI can write (the picker sets the in-memory session only,
|
|
8
|
+
// and `sys_user.domain` is write-protected even for admin). So jsn stores
|
|
9
|
+
// the chosen domain in the active profile and sends it on every request
|
|
10
|
+
// via the `X-Now-Domain` header — the same scoping the platform applies
|
|
11
|
+
// to its own picker sessions.
|
|
12
|
+
|
|
13
|
+
import { getStringField, interactiveList, assertSafeExactMatch } from '../../helpers.js';
|
|
14
|
+
|
|
15
|
+
const DETAIL_FIELDS = 'sys_id,name,parent,sys_domain_path,description,active,sys_created_on,sys_updated_on';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gate: domain separation must be installed. The `domain` table only
|
|
19
|
+
* exists on instances with the plugin. A single cheap query detects it;
|
|
20
|
+
* on non-DS instances the API returns "Invalid table domain".
|
|
21
|
+
*/
|
|
22
|
+
export async function isDomainSeparationInstalled(app) {
|
|
23
|
+
try {
|
|
24
|
+
const params = new URLSearchParams();
|
|
25
|
+
params.set('sysparm_limit', '1');
|
|
26
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
27
|
+
await app.sdk.list('domain', params);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function requireDomainSeparation(app) {
|
|
35
|
+
if (!(await isDomainSeparationInstalled(app))) {
|
|
36
|
+
const e = new Error('Domain separation is not installed on this instance — the `domain` table does not exist. Install the Domain Separation plugin to use these commands.');
|
|
37
|
+
e.name = 'NotSupported';
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a domain name or sys_id to its sys_id. Throws when not found.
|
|
44
|
+
*/
|
|
45
|
+
export async function resolveDomainSysId(app, domainArg) {
|
|
46
|
+
assertSafeExactMatch(domainArg);
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
params.set('sysparm_query', `name=${domainArg}`);
|
|
49
|
+
params.set('sysparm_limit', '1');
|
|
50
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
51
|
+
let records = await app.sdk.list('domain', params);
|
|
52
|
+
if (records.length === 0) {
|
|
53
|
+
const p2 = new URLSearchParams();
|
|
54
|
+
p2.set('sysparm_query', `sys_id=${domainArg}`);
|
|
55
|
+
p2.set('sysparm_limit', '1');
|
|
56
|
+
p2.set('sysparm_fields', 'sys_id,name');
|
|
57
|
+
records = await app.sdk.list('domain', p2);
|
|
58
|
+
}
|
|
59
|
+
if (records.length === 0) {
|
|
60
|
+
throw new Error(`Domain not found: ${domainArg}`);
|
|
61
|
+
}
|
|
62
|
+
return getStringField(records[0], 'sys_id');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Interactive domain picker — returns the chosen domain's sys_id, or null
|
|
67
|
+
* when the user cancels. Used by `domains set` and the setup/modify flows.
|
|
68
|
+
*/
|
|
69
|
+
export async function pickDomain(app) {
|
|
70
|
+
const picked = await interactiveList({
|
|
71
|
+
app, table: 'domain', singular: 'domain', columns: ['name', 'sys_domain_path'], labelField: 'name',
|
|
72
|
+
formatLabel: formatDomainLabel,
|
|
73
|
+
});
|
|
74
|
+
if (!picked) return null;
|
|
75
|
+
return getStringField(picked, 'sys_id');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Format a picker label: "Name (path)". */
|
|
79
|
+
export function formatDomainLabel(r) {
|
|
80
|
+
const path = getStringField(r, 'sys_domain_path');
|
|
81
|
+
return `${getStringField(r, 'name')}${path ? ` (${path})` : ''}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Build a rich detail view for a domain record. */
|
|
85
|
+
export async function formatDomainDetail(app, record) {
|
|
86
|
+
const sysID = getStringField(record, 'sys_id');
|
|
87
|
+
const name = getStringField(record, 'name');
|
|
88
|
+
const instance = app.getEffectiveInstance();
|
|
89
|
+
const link = `${instance}/domain.do?sys_id=${sysID}`;
|
|
90
|
+
|
|
91
|
+
// Fetch full record for fields not present on the picked row
|
|
92
|
+
const params = new URLSearchParams();
|
|
93
|
+
params.set('sysparm_query', `sys_id=${sysID}`);
|
|
94
|
+
params.set('sysparm_limit', '1');
|
|
95
|
+
params.set('sysparm_display_value', 'all');
|
|
96
|
+
params.set('sysparm_fields', DETAIL_FIELDS);
|
|
97
|
+
const full = await app.sdk.list('domain', params);
|
|
98
|
+
const rec = full[0] || record;
|
|
99
|
+
|
|
100
|
+
const parent = getStringField(rec, 'parent');
|
|
101
|
+
const path = getStringField(rec, 'sys_domain_path');
|
|
102
|
+
|
|
103
|
+
const lines = [
|
|
104
|
+
`Domain: ${name}`,
|
|
105
|
+
`Path: ${path || '(root)'}`,
|
|
106
|
+
`Parent: ${parent || '(none)'}`,
|
|
107
|
+
`Active: ${getStringField(rec, 'active') || 'true'}`,
|
|
108
|
+
`Created: ${getStringField(rec, 'sys_created_on')}`,
|
|
109
|
+
`Updated: ${getStringField(rec, 'sys_updated_on')}`,
|
|
110
|
+
`Link: ${link}`,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const desc = getStringField(rec, 'description');
|
|
114
|
+
if (desc) lines.splice(1, 0, `Desc: ${desc}`);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
sys_id: sysID,
|
|
118
|
+
name,
|
|
119
|
+
parent,
|
|
120
|
+
sys_domain_path: path,
|
|
121
|
+
_formatted: lines.join('\n'),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function domainsCmd(wrap) {
|
|
126
|
+
return {
|
|
127
|
+
command: 'domains [subcommand]',
|
|
128
|
+
aliases: ['domain', 'dom'],
|
|
129
|
+
describe: 'Manage domains (domain separation)',
|
|
130
|
+
builder: (yargs) => {
|
|
131
|
+
return yargs
|
|
132
|
+
.command({
|
|
133
|
+
command: 'list',
|
|
134
|
+
aliases: ['ls'],
|
|
135
|
+
describe: 'List domains (hierarchy + paths)',
|
|
136
|
+
builder: (y) => y
|
|
137
|
+
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEACME")' })
|
|
138
|
+
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns' })
|
|
139
|
+
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
|
|
140
|
+
handler: wrap(async (argv, app) => {
|
|
141
|
+
await requireDomainSeparation(app);
|
|
142
|
+
const columns = argv.columns ? argv.columns.split(',') : ['name', 'parent', 'sys_domain_path'];
|
|
143
|
+
const query = argv.query || '';
|
|
144
|
+
|
|
145
|
+
const picked = await interactiveList({
|
|
146
|
+
app, table: 'domain', singular: 'domain', columns, limit: argv.limit, query, labelField: 'name',
|
|
147
|
+
formatLabel: formatDomainLabel,
|
|
148
|
+
});
|
|
149
|
+
if (picked === undefined) return; // user cancelled
|
|
150
|
+
if (picked) {
|
|
151
|
+
const detail = await formatDomainDetail(app, picked);
|
|
152
|
+
return app.ok(detail, { summary: `Domain: ${getStringField(picked, 'name')}` });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const params = new URLSearchParams();
|
|
156
|
+
params.set('sysparm_limit', String(argv.limit));
|
|
157
|
+
params.set('sysparm_display_value', 'all');
|
|
158
|
+
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
159
|
+
const q = argv.query ? argv.query + '^ORDERBYsys_domain_path' : 'ORDERBYsys_domain_path';
|
|
160
|
+
params.set('sysparm_query', q);
|
|
161
|
+
const records = await app.sdk.list('domain', params);
|
|
162
|
+
app.ok({
|
|
163
|
+
table: 'domain',
|
|
164
|
+
count: records.length,
|
|
165
|
+
columns,
|
|
166
|
+
records: records.map(r => formatDomainListRecord(r, columns)),
|
|
167
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
168
|
+
}, { summary: `${records.length} domain(s)` });
|
|
169
|
+
}),
|
|
170
|
+
})
|
|
171
|
+
.command({
|
|
172
|
+
command: 'show <domain>',
|
|
173
|
+
aliases: ['get'],
|
|
174
|
+
describe: 'Show a domain by name or sys_id',
|
|
175
|
+
builder: (y) => y
|
|
176
|
+
.positional('domain', {
|
|
177
|
+
describe: 'Domain name (e.g. ACME) or sys_id',
|
|
178
|
+
type: 'string',
|
|
179
|
+
}),
|
|
180
|
+
handler: wrap(async (argv, app) => {
|
|
181
|
+
await requireDomainSeparation(app);
|
|
182
|
+
assertSafeExactMatch(argv.domain);
|
|
183
|
+
let records;
|
|
184
|
+
// Try by name first, then by sys_id
|
|
185
|
+
for (const queryField of ['name', 'sys_id']) {
|
|
186
|
+
const params = new URLSearchParams();
|
|
187
|
+
params.set('sysparm_query', `${queryField}=${argv.domain}`);
|
|
188
|
+
params.set('sysparm_limit', '1');
|
|
189
|
+
params.set('sysparm_display_value', 'all');
|
|
190
|
+
records = await app.sdk.list('domain', params);
|
|
191
|
+
if (records.length > 0) break;
|
|
192
|
+
}
|
|
193
|
+
if (!records || records.length === 0) {
|
|
194
|
+
throw new Error(`Domain not found: ${argv.domain}`);
|
|
195
|
+
}
|
|
196
|
+
const detail = await formatDomainDetail(app, records[0]);
|
|
197
|
+
app.ok(detail, { summary: `Domain ${argv.domain}` });
|
|
198
|
+
}),
|
|
199
|
+
})
|
|
200
|
+
.command({
|
|
201
|
+
command: 'current',
|
|
202
|
+
describe: 'Show the configured domain for this profile',
|
|
203
|
+
handler: wrap(async (argv, app) => {
|
|
204
|
+
await requireDomainSeparation(app);
|
|
205
|
+
const domain = app._profileDomain(app.config);
|
|
206
|
+
if (!domain) {
|
|
207
|
+
app.ok({ domain: '', scoping: 'none' }, {
|
|
208
|
+
summary: 'No domain configured — requests are not domain-scoped',
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Resolve the configured value to a display name if it's a sys_id
|
|
213
|
+
let label = domain;
|
|
214
|
+
try {
|
|
215
|
+
const params = new URLSearchParams();
|
|
216
|
+
params.set('sysparm_query', `sys_id=${domain}`);
|
|
217
|
+
params.set('sysparm_limit', '1');
|
|
218
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
219
|
+
const records = await app.sdk.list('domain', params);
|
|
220
|
+
if (records.length > 0) label = `${getStringField(records[0], 'name')} (${domain})`;
|
|
221
|
+
} catch { /* non-fatal — show raw value */ }
|
|
222
|
+
app.ok({ domain, scoping: 'X-Now-Domain header' }, {
|
|
223
|
+
summary: `Configured domain: ${label}`,
|
|
224
|
+
});
|
|
225
|
+
}),
|
|
226
|
+
})
|
|
227
|
+
.command({
|
|
228
|
+
command: 'set [domain]',
|
|
229
|
+
describe: 'Set the configured domain for this profile (interactive picker when run bare; omit to clear)',
|
|
230
|
+
handler: wrap(async (argv, app) => {
|
|
231
|
+
await requireDomainSeparation(app);
|
|
232
|
+
let domainArg = argv.domain;
|
|
233
|
+
if (!domainArg) {
|
|
234
|
+
const picked = await pickDomain(app);
|
|
235
|
+
if (!picked) return; // cancelled or non-interactive
|
|
236
|
+
domainArg = picked;
|
|
237
|
+
} else if (domainArg.toLowerCase() === 'none' || domainArg.toLowerCase() === 'clear') {
|
|
238
|
+
app.setDomain('');
|
|
239
|
+
app.ok({ domain: '' }, { summary: 'Domain cleared — requests are no longer domain-scoped' });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sysID = await resolveDomainSysId(app, domainArg);
|
|
244
|
+
app.setDomain(sysID);
|
|
245
|
+
app.ok({ domain: sysID, name: domainArg }, {
|
|
246
|
+
summary: `Configured domain: ${domainArg} — all requests scoped to it`,
|
|
247
|
+
});
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
handler: async (argv) => {
|
|
252
|
+
if (!argv._[1]) {
|
|
253
|
+
const app = argv.app;
|
|
254
|
+
let installed = false;
|
|
255
|
+
try {
|
|
256
|
+
if (app && app.sdk) installed = await isDomainSeparationInstalled(app);
|
|
257
|
+
} catch {
|
|
258
|
+
installed = false;
|
|
259
|
+
}
|
|
260
|
+
if (!installed) {
|
|
261
|
+
if (app && app.sdk) {
|
|
262
|
+
console.log('Domain separation is not installed on this instance.');
|
|
263
|
+
console.log('Install the Domain Separation plugin to manage domains.');
|
|
264
|
+
} else {
|
|
265
|
+
console.log('No instance configured — run "jsn auth login <instance>" first.');
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log('Manage ServiceNow domains (domain separation).\n');
|
|
270
|
+
console.log('Commands:');
|
|
271
|
+
console.log(' list List domains (hierarchy + paths)');
|
|
272
|
+
console.log(' show <domain> Show a domain');
|
|
273
|
+
console.log(' current Show the configured domain for this profile');
|
|
274
|
+
console.log(' set [domain] Configure the domain for this profile (picker when bare; "clear" to unset)');
|
|
275
|
+
console.log('\nDomains are applied per-request via the X-Now-Domain header —');
|
|
276
|
+
console.log('the platform filters every query to the domain and its children.');
|
|
277
|
+
console.log('Run "jsn domains <command> --help" for details.');
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Flat display record for list mode (keeps the JSON payload small). */
|
|
284
|
+
function formatDomainListRecord(r, columns) {
|
|
285
|
+
const out = {};
|
|
286
|
+
for (const c of columns) {
|
|
287
|
+
const v = getStringField(r, c);
|
|
288
|
+
out[c] = v;
|
|
289
|
+
}
|
|
290
|
+
out.sys_id = getStringField(r, 'sys_id');
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
@@ -1,4 +1,66 @@
|
|
|
1
|
-
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList, assertSafeExactMatch } from '../../helpers.js';
|
|
2
|
+
import { requireCurrentUserSysId, setCurrentApplication } from '../../context.js';
|
|
3
|
+
|
|
4
|
+
/** Format a picker label for a scope: "Name [scope]". */
|
|
5
|
+
export function formatScopeLabel(r) {
|
|
6
|
+
return `${getStringField(r, 'name')} [${getStringField(r, 'scope') || '?'}]`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rich display for a scope: header fields + record link.
|
|
11
|
+
* Shared by `list` pick and `show` so they can't drift.
|
|
12
|
+
*/
|
|
13
|
+
export async function formatScopeDetail(app, record) {
|
|
14
|
+
const sysID = getStringField(record, 'sys_id');
|
|
15
|
+
const instance = app.getEffectiveInstance();
|
|
16
|
+
const link = `${instance}/sys_scope.do?sys_id=${sysID}`;
|
|
17
|
+
|
|
18
|
+
let full = record;
|
|
19
|
+
try {
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
params.set('sysparm_query', `sys_id=${sysID}`);
|
|
22
|
+
params.set('sysparm_limit', '1');
|
|
23
|
+
params.set('sysparm_display_value', 'all');
|
|
24
|
+
params.set('sysparm_fields', 'sys_id,name,scope,short_description,active,description,version,release_date,sys_class_name,sys_created_on,sys_updated_on');
|
|
25
|
+
const recs = await app.sdk.list('sys_scope', params);
|
|
26
|
+
if (Array.isArray(recs) && recs.length > 0) full = recs[0];
|
|
27
|
+
} catch {
|
|
28
|
+
// fall back to the passed record
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Store apps carry sys_class_name "Store Application" — link to the
|
|
32
|
+
// Store search for the scope value (no store-side app ID is available
|
|
33
|
+
// locally; the search URL is the reliable path).
|
|
34
|
+
const isStoreApp = String(getStringField(full, 'sys_class_name')).toLowerCase().includes('store');
|
|
35
|
+
const storeLink = isStoreApp ? `https://store.servicenow.com/store/apps?q=${encodeURIComponent(getStringField(full, 'scope'))}` : '';
|
|
36
|
+
|
|
37
|
+
const lines = [];
|
|
38
|
+
lines.push(`Scope: ${getStringField(full, 'name') || '?'}`);
|
|
39
|
+
lines.push(` Scope value: ${getStringField(full, 'scope') || '?'}`);
|
|
40
|
+
if (getStringField(full, 'version')) lines.push(` Version: ${getStringField(full, 'version')}`);
|
|
41
|
+
if (getStringField(full, 'release_date')) lines.push(` Released: ${getStringField(full, 'release_date')}`);
|
|
42
|
+
if (getStringField(full, 'short_description')) lines.push(` Description: ${getStringField(full, 'short_description')}`);
|
|
43
|
+
if (getStringField(full, 'active')) lines.push(` Active: ${getStringField(full, 'active')}`);
|
|
44
|
+
if (storeLink) lines.push(` Store: ${storeLink}`);
|
|
45
|
+
if (getStringField(full, 'sys_created_on')) lines.push(` Created: ${getStringField(full, 'sys_created_on')}`);
|
|
46
|
+
if (getStringField(full, 'sys_updated_on')) lines.push(` Updated: ${getStringField(full, 'sys_updated_on')}`);
|
|
47
|
+
lines.push(` Link: ${link}`);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
sys_id: sysID,
|
|
51
|
+
name: getStringField(full, 'name'),
|
|
52
|
+
scope: getStringField(full, 'scope'),
|
|
53
|
+
version: getStringField(full, 'version') || '',
|
|
54
|
+
release_date: getStringField(full, 'release_date') || '',
|
|
55
|
+
short_description: getStringField(full, 'short_description') || '',
|
|
56
|
+
active: getStringField(full, 'active') || '',
|
|
57
|
+
store_link: storeLink,
|
|
58
|
+
sys_created_on: getStringField(full, 'sys_created_on') || '',
|
|
59
|
+
sys_updated_on: getStringField(full, 'sys_updated_on') || '',
|
|
60
|
+
link,
|
|
61
|
+
_formatted: lines.join('\n'),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
2
64
|
|
|
3
65
|
export function scopesCmd(wrap) {
|
|
4
66
|
return {
|
|
@@ -21,12 +83,12 @@ export function scopesCmd(wrap) {
|
|
|
21
83
|
|
|
22
84
|
const picked = await interactiveList({
|
|
23
85
|
app, table: 'sys_scope', singular: 'scope', columns, limit: argv.limit, query, labelField: 'name',
|
|
24
|
-
formatLabel:
|
|
86
|
+
formatLabel: formatScopeLabel,
|
|
25
87
|
});
|
|
26
88
|
if (picked === undefined) return; // user cancelled
|
|
27
89
|
if (picked) {
|
|
28
|
-
|
|
29
|
-
return app.ok(
|
|
90
|
+
const detail = await formatScopeDetail(app, picked);
|
|
91
|
+
return app.ok(detail, { summary: `Scope: ${getStringField(picked, 'name')}` });
|
|
30
92
|
}
|
|
31
93
|
|
|
32
94
|
const params = new URLSearchParams();
|
|
@@ -55,6 +117,7 @@ export function scopesCmd(wrap) {
|
|
|
55
117
|
type: 'string',
|
|
56
118
|
}),
|
|
57
119
|
handler: wrap(async (argv, app) => {
|
|
120
|
+
assertSafeExactMatch(argv.scope);
|
|
58
121
|
let records;
|
|
59
122
|
// Try by scope value first, then by name
|
|
60
123
|
for (const queryField of ['scope', 'name']) {
|
|
@@ -68,47 +131,36 @@ export function scopesCmd(wrap) {
|
|
|
68
131
|
if (!records || records.length === 0) {
|
|
69
132
|
throw new Error(`Scope not found: ${argv.scope}`);
|
|
70
133
|
}
|
|
71
|
-
app
|
|
134
|
+
const detail = await formatScopeDetail(app, records[0]);
|
|
135
|
+
app.ok(detail, { summary: `Scope ${argv.scope}` });
|
|
72
136
|
}),
|
|
73
137
|
})
|
|
74
138
|
.command({
|
|
75
|
-
command: 'set
|
|
76
|
-
describe: 'Set the current application scope',
|
|
139
|
+
command: 'set [scope]',
|
|
140
|
+
describe: 'Set the current application scope (interactive picker when run bare)',
|
|
77
141
|
handler: wrap(async (argv, app) => {
|
|
142
|
+
let scopeArg = argv.scope;
|
|
143
|
+
if (!scopeArg) {
|
|
144
|
+
const picked = await interactiveList({
|
|
145
|
+
app, table: 'sys_scope', singular: 'scope', columns: ['name', 'scope'], labelField: 'name',
|
|
146
|
+
formatLabel: r => `${getStringField(r, 'name')} [${getStringField(r, 'scope') || '?'}]`,
|
|
147
|
+
});
|
|
148
|
+
if (!picked) return; // cancelled or non-interactive
|
|
149
|
+
scopeArg = getStringField(picked, 'scope');
|
|
150
|
+
}
|
|
151
|
+
assertSafeExactMatch(scopeArg);
|
|
78
152
|
const params = new URLSearchParams();
|
|
79
|
-
params.set('sysparm_query', `scope=${
|
|
153
|
+
params.set('sysparm_query', `scope=${scopeArg}`);
|
|
80
154
|
params.set('sysparm_limit', '1');
|
|
81
|
-
params.set('sysparm_fields', 'sys_id,scope');
|
|
155
|
+
params.set('sysparm_fields', 'sys_id,scope,name');
|
|
82
156
|
const records = await app.sdk.list('sys_scope', params);
|
|
83
157
|
if (records.length === 0) {
|
|
84
|
-
throw new Error(`Scope not found: ${
|
|
158
|
+
throw new Error(`Scope not found: ${scopeArg}`);
|
|
85
159
|
}
|
|
86
160
|
const scopeSysID = getStringField(records[0], 'sys_id');
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
sysparm_limit: '1',
|
|
91
|
-
sysparm_fields: 'sys_id',
|
|
92
|
-
}));
|
|
93
|
-
if (user.length === 0) {
|
|
94
|
-
throw new Error('Could not determine current user');
|
|
95
|
-
}
|
|
96
|
-
const userSysID = getStringField(user[0], 'sys_id');
|
|
97
|
-
const prefParams = new URLSearchParams();
|
|
98
|
-
prefParams.set('sysparm_query', `user=${userSysID}^name=apps.current_app`);
|
|
99
|
-
prefParams.set('sysparm_limit', '1');
|
|
100
|
-
const prefs = await app.sdk.list('sys_user_preference', prefParams);
|
|
101
|
-
if (prefs.length > 0) {
|
|
102
|
-
await app.sdk.update('sys_user_preference', getStringField(prefs[0], 'sys_id'), { value: scopeSysID });
|
|
103
|
-
} else {
|
|
104
|
-
await app.sdk.create('sys_user_preference', {
|
|
105
|
-
user: userSysID,
|
|
106
|
-
name: 'apps.current_app',
|
|
107
|
-
value: scopeSysID,
|
|
108
|
-
type: 'string',
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
app.ok({ scope: argv.scope, sys_id: scopeSysID }, { summary: `Current scope: ${argv.scope}` });
|
|
161
|
+
const userSysID = await requireCurrentUserSysId(app.sdk);
|
|
162
|
+
await setCurrentApplication(app.sdk, userSysID, scopeSysID);
|
|
163
|
+
app.ok({ scope: scopeArg, sys_id: scopeSysID }, { summary: `Current scope: ${scopeArg}` });
|
|
112
164
|
}),
|
|
113
165
|
})
|
|
114
166
|
.command({
|
|
@@ -158,7 +210,7 @@ export function scopesCmd(wrap) {
|
|
|
158
210
|
console.log('Commands:');
|
|
159
211
|
console.log(' list List application scopes');
|
|
160
212
|
console.log(' show <scope> Show a scope');
|
|
161
|
-
console.log(' set
|
|
213
|
+
console.log(' set [scope] Set the current application scope (picker when run bare)');
|
|
162
214
|
console.log(' create Create a new application scope');
|
|
163
215
|
console.log('\nRun "jsn scopes <command> --help" for details.');
|
|
164
216
|
}
|