@jacebenson/jsn 0.0.10 → 1.0.2
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 +7 -49
- package/bin/jsn.js +57 -2
- package/package.json +28 -32
- package/scripts/install.sh +227 -0
- package/scripts/npm-install.js +235 -0
- package/scripts/pre-commit-check.sh +61 -0
- package/src/app.js +0 -157
- package/src/auth.js +0 -283
- package/src/cli.js +0 -144
- package/src/commands/_ticket.js +0 -256
- package/src/commands/auth.js +0 -62
- package/src/commands/changes.js +0 -7
- package/src/commands/dev/_generic.js +0 -223
- package/src/commands/dev/_simple.js +0 -89
- package/src/commands/dev/eval.js +0 -17
- package/src/commands/dev/flows.js +0 -528
- package/src/commands/dev/forms.js +0 -313
- package/src/commands/dev/lists.js +0 -233
- package/src/commands/dev/logs.js +0 -51
- package/src/commands/dev/rest.js +0 -64
- package/src/commands/dev/scopes.js +0 -96
- package/src/commands/dev/updatesets.js +0 -97
- package/src/commands/dev.js +0 -53
- package/src/commands/groupmembers.js +0 -39
- package/src/commands/grouproles.js +0 -39
- package/src/commands/groups.js +0 -57
- package/src/commands/incidents.js +0 -7
- package/src/commands/profiles.js +0 -79
- package/src/commands/records.js +0 -137
- package/src/commands/requests.js +0 -7
- package/src/commands/setup.js +0 -39
- package/src/commands/tasks.js +0 -7
- package/src/commands/tickets.js +0 -121
- package/src/commands/users.js +0 -57
- package/src/commands/version.js +0 -25
- package/src/config.js +0 -154
- package/src/context.js +0 -62
- package/src/errors.js +0 -101
- package/src/helpers.js +0 -60
- package/src/output.js +0 -410
- package/src/sdk.js +0 -357
package/src/commands/tickets.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { formatRecordForDisplay, getStringField, buildQuerySuffix } from '../helpers.js';
|
|
2
|
-
|
|
3
|
-
export function ticketsCmd(wrap) {
|
|
4
|
-
return {
|
|
5
|
-
command: 'tickets [subcommand]',
|
|
6
|
-
aliases: ['ticket'],
|
|
7
|
-
describe: 'Query generic tickets (e.g. "tickets list --query active=true")',
|
|
8
|
-
builder: (yargs) => {
|
|
9
|
-
return yargs
|
|
10
|
-
.command({
|
|
11
|
-
command: 'list',
|
|
12
|
-
aliases: ['ls'],
|
|
13
|
-
describe: 'List tickets',
|
|
14
|
-
builder: (y) => y
|
|
15
|
-
.option('query', { type: 'string', describe: 'Encoded query (e.g. "active=true^priority=1")' })
|
|
16
|
-
.option('columns', { alias: 'c', type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description,priority")' })
|
|
17
|
-
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' })
|
|
18
|
-
.option('offset', { alias: 'o', type: 'number', default: 0, describe: 'Offset' }),
|
|
19
|
-
handler: wrap(async (argv, app) => {
|
|
20
|
-
const columns = argv.columns ? argv.columns.split(',') : ['number', 'short_description', 'state', 'assigned_to'];
|
|
21
|
-
const params = new URLSearchParams();
|
|
22
|
-
params.set('sysparm_limit', String(argv.limit));
|
|
23
|
-
params.set('sysparm_offset', String(argv.offset));
|
|
24
|
-
params.set('sysparm_display_value', 'all');
|
|
25
|
-
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
26
|
-
const q = argv.query ? argv.query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
|
|
27
|
-
params.set('sysparm_query', q);
|
|
28
|
-
const records = await app.sdk.list('ticket', params);
|
|
29
|
-
const displayRecords = records.map(r => formatRecordForDisplay(r, columns));
|
|
30
|
-
const breadcrumbs = [
|
|
31
|
-
{ action: 'create', cmd: 'jsn tickets create --data \'{...}\'', description: 'Create a new ticket' },
|
|
32
|
-
];
|
|
33
|
-
if (records.length === argv.limit) {
|
|
34
|
-
breadcrumbs.push({
|
|
35
|
-
action: 'next',
|
|
36
|
-
cmd: `jsn tickets list --limit ${argv.limit} --offset ${argv.offset + argv.limit}${buildQuerySuffix(argv.query)}`,
|
|
37
|
-
description: `Next page`,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
if (argv.offset > 0) {
|
|
41
|
-
breadcrumbs.push({
|
|
42
|
-
action: 'prev',
|
|
43
|
-
cmd: `jsn tickets list --limit ${argv.limit} --offset ${Math.max(0, argv.offset - argv.limit)}${buildQuerySuffix(argv.query)}`,
|
|
44
|
-
description: 'Previous page',
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
app.ok({
|
|
48
|
-
table: 'ticket',
|
|
49
|
-
count: records.length,
|
|
50
|
-
columns,
|
|
51
|
-
records: displayRecords,
|
|
52
|
-
pagination: { limit: argv.limit, offset: argv.offset },
|
|
53
|
-
context: { instance_url: app.getEffectiveInstance() },
|
|
54
|
-
}, { summary: `${records.length} ticket(s)`, breadcrumbs });
|
|
55
|
-
}),
|
|
56
|
-
})
|
|
57
|
-
.command({
|
|
58
|
-
command: 'show <number>',
|
|
59
|
-
aliases: ['get'],
|
|
60
|
-
describe: 'Show a ticket',
|
|
61
|
-
handler: wrap(async (argv, app) => {
|
|
62
|
-
const params = new URLSearchParams();
|
|
63
|
-
params.set('sysparm_query', `number=${argv.number}`);
|
|
64
|
-
params.set('sysparm_limit', '1');
|
|
65
|
-
params.set('sysparm_display_value', 'all');
|
|
66
|
-
const records = await app.sdk.list('ticket', params);
|
|
67
|
-
if (records.length === 0) {
|
|
68
|
-
throw new Error(`Ticket not found: ${argv.number}`);
|
|
69
|
-
}
|
|
70
|
-
app.ok(records[0], { summary: `Ticket ${argv.number}` });
|
|
71
|
-
}),
|
|
72
|
-
})
|
|
73
|
-
.command({
|
|
74
|
-
command: 'create',
|
|
75
|
-
describe: 'Create a new ticket',
|
|
76
|
-
builder: (y) => y.option('data', { type: 'string', demandOption: true, describe: 'JSON fields (e.g. \'{"state":"2","priority":"1"}\')' }),
|
|
77
|
-
handler: wrap(async (argv, app) => {
|
|
78
|
-
const recordData = JSON.parse(argv.data);
|
|
79
|
-
const record = await app.sdk.create('ticket', recordData);
|
|
80
|
-
app.ok(record, { summary: `Created ticket ${getStringField(record, 'number')}` });
|
|
81
|
-
}),
|
|
82
|
-
})
|
|
83
|
-
.command({
|
|
84
|
-
command: 'update <number>',
|
|
85
|
-
describe: 'Update a ticket',
|
|
86
|
-
builder: (y) => y.option('data', { type: 'string', demandOption: true, describe: 'JSON fields (e.g. \'{"state":"2","priority":"1"}\')' }),
|
|
87
|
-
handler: wrap(async (argv, app) => {
|
|
88
|
-
const recordData = JSON.parse(argv.data);
|
|
89
|
-
const findParams = new URLSearchParams();
|
|
90
|
-
findParams.set('sysparm_query', `number=${argv.number}`);
|
|
91
|
-
findParams.set('sysparm_limit', '1');
|
|
92
|
-
const records = await app.sdk.list('ticket', findParams);
|
|
93
|
-
if (records.length === 0) {
|
|
94
|
-
throw new Error(`Ticket not found: ${argv.number}`);
|
|
95
|
-
}
|
|
96
|
-
const sysID = getStringField(records[0], 'sys_id');
|
|
97
|
-
const updated = await app.sdk.update('ticket', sysID, recordData);
|
|
98
|
-
app.ok(updated, { summary: `Updated ticket ${argv.number}` });
|
|
99
|
-
}),
|
|
100
|
-
})
|
|
101
|
-
.command({
|
|
102
|
-
command: 'delete <number>',
|
|
103
|
-
describe: 'Delete a ticket',
|
|
104
|
-
handler: wrap(async (argv, app) => {
|
|
105
|
-
const findParams = new URLSearchParams();
|
|
106
|
-
findParams.set('sysparm_query', `number=${argv.number}`);
|
|
107
|
-
findParams.set('sysparm_limit', '1');
|
|
108
|
-
const records = await app.sdk.list('ticket', findParams);
|
|
109
|
-
if (records.length === 0) {
|
|
110
|
-
throw new Error(`Ticket not found: ${argv.number}`);
|
|
111
|
-
}
|
|
112
|
-
const sysID = getStringField(records[0], 'sys_id');
|
|
113
|
-
await app.sdk.delete('ticket', sysID);
|
|
114
|
-
app.ok({ number: argv.number, message: 'Ticket deleted' }, { summary: `Deleted ticket ${argv.number}` });
|
|
115
|
-
}),
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
},
|
|
119
|
-
handler: () => {},
|
|
120
|
-
};
|
|
121
|
-
}
|
package/src/commands/users.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { formatRecordForDisplay } from '../helpers.js';
|
|
2
|
-
|
|
3
|
-
export function usersCmd(wrap) {
|
|
4
|
-
return {
|
|
5
|
-
command: 'users [subcommand]',
|
|
6
|
-
aliases: ['user'],
|
|
7
|
-
describe: 'Search and display users',
|
|
8
|
-
builder: (yargs) => {
|
|
9
|
-
return yargs
|
|
10
|
-
.command({
|
|
11
|
-
command: 'list',
|
|
12
|
-
aliases: ['ls'],
|
|
13
|
-
describe: 'List users',
|
|
14
|
-
builder: (y) => y
|
|
15
|
-
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
|
|
16
|
-
.option('columns', { alias: 'c', 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(',') : ['user_name', 'name', 'email', 'active'];
|
|
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
|
-
if (argv.query) params.set('sysparm_query', argv.query);
|
|
25
|
-
const records = await app.sdk.list('sys_user', params);
|
|
26
|
-
app.ok({
|
|
27
|
-
table: 'sys_user',
|
|
28
|
-
count: records.length,
|
|
29
|
-
columns,
|
|
30
|
-
records: records.map(r => formatRecordForDisplay(r, columns)),
|
|
31
|
-
context: { instance_url: app.getEffectiveInstance() },
|
|
32
|
-
}, { summary: `${records.length} user(s)` });
|
|
33
|
-
}),
|
|
34
|
-
})
|
|
35
|
-
.command({
|
|
36
|
-
command: 'show <identifier>',
|
|
37
|
-
aliases: ['get'],
|
|
38
|
-
describe: 'Show a user by user_name or sys_id',
|
|
39
|
-
handler: wrap(async (argv, app) => {
|
|
40
|
-
const id = argv.identifier;
|
|
41
|
-
const isSysID = id.length === 32 && /^[0-9a-fA-F]+$/.test(id);
|
|
42
|
-
const params = new URLSearchParams();
|
|
43
|
-
params.set('sysparm_query', isSysID ? `sys_id=${id}` : `user_name=${id}`);
|
|
44
|
-
params.set('sysparm_limit', '1');
|
|
45
|
-
params.set('sysparm_display_value', 'all');
|
|
46
|
-
const records = await app.sdk.list('sys_user', params);
|
|
47
|
-
if (records.length === 0) {
|
|
48
|
-
throw new Error(`User not found: ${id}`);
|
|
49
|
-
}
|
|
50
|
-
app.ok(records[0], { summary: `User ${id}` });
|
|
51
|
-
}),
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
handler: () => {},
|
|
56
|
-
};
|
|
57
|
-
}
|
package/src/commands/version.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
function getVersion() {
|
|
6
|
-
try {
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
9
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
10
|
-
return pkg.version;
|
|
11
|
-
} catch {
|
|
12
|
-
return '0.0.0';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function versionCmd(wrap) {
|
|
17
|
-
return {
|
|
18
|
-
command: 'version',
|
|
19
|
-
describe: 'Show version information',
|
|
20
|
-
handler: wrap(async (_argv, app) => {
|
|
21
|
-
const version = getVersion();
|
|
22
|
-
app.ok({ version }, { summary: `jsn ${version}` });
|
|
23
|
-
}),
|
|
24
|
-
};
|
|
25
|
-
}
|
package/src/config.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// Layered configuration: flags > env > local > global > defaults
|
|
2
|
-
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
|
|
7
|
-
const APP_NAME = 'servicenow';
|
|
8
|
-
|
|
9
|
-
export function globalConfigDir() {
|
|
10
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
11
|
-
if (xdg) return path.join(xdg, APP_NAME);
|
|
12
|
-
return path.join(os.homedir(), '.config', APP_NAME);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function globalConfigPath() {
|
|
16
|
-
return path.join(globalConfigDir(), 'config.json');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function localConfigPath() {
|
|
20
|
-
return path.join(process.cwd(), `.${APP_NAME}`, 'config.json');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function cacheDir() {
|
|
24
|
-
const xdg = process.env.XDG_CACHE_HOME;
|
|
25
|
-
if (xdg) return path.join(xdg, APP_NAME);
|
|
26
|
-
return path.join(os.homedir(), '.cache', APP_NAME);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function normalizeInstanceURL(url) {
|
|
30
|
-
if (!url) return '';
|
|
31
|
-
url = url.trim();
|
|
32
|
-
url = url.replace(/\/$/, '');
|
|
33
|
-
if (!/^https?:\/\//i.test(url)) {
|
|
34
|
-
url = 'https://' + url;
|
|
35
|
-
}
|
|
36
|
-
return url;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function loadFromFile(cfg, filePath, source) {
|
|
40
|
-
try {
|
|
41
|
-
const data = fs.readFileSync(filePath, 'utf-8');
|
|
42
|
-
const fileCfg = JSON.parse(data);
|
|
43
|
-
if (fileCfg.instance_url) {
|
|
44
|
-
cfg.instanceURL = fileCfg.instance_url;
|
|
45
|
-
cfg.sources.instance_url = source;
|
|
46
|
-
}
|
|
47
|
-
if (fileCfg.format) {
|
|
48
|
-
cfg.format = fileCfg.format;
|
|
49
|
-
cfg.sources.format = source;
|
|
50
|
-
}
|
|
51
|
-
if (fileCfg.default_profile) {
|
|
52
|
-
cfg.defaultProfile = fileCfg.default_profile;
|
|
53
|
-
cfg.activeProfile = fileCfg.default_profile;
|
|
54
|
-
cfg.sources.default_profile = source;
|
|
55
|
-
}
|
|
56
|
-
if (fileCfg.profiles && typeof fileCfg.profiles === 'object') {
|
|
57
|
-
cfg.profiles = { ...cfg.profiles, ...fileCfg.profiles };
|
|
58
|
-
cfg.sources.profiles = source;
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// File doesn't exist or is malformed — skip
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function loadFromEnv(cfg) {
|
|
66
|
-
if (process.env.SERVICENOW_INSTANCE_URL) {
|
|
67
|
-
cfg.instanceURL = process.env.SERVICENOW_INSTANCE_URL;
|
|
68
|
-
cfg.sources.instance_url = 'env';
|
|
69
|
-
}
|
|
70
|
-
if (process.env.SERVICENOW_FORMAT) {
|
|
71
|
-
cfg.format = process.env.SERVICENOW_FORMAT;
|
|
72
|
-
cfg.sources.format = 'env';
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function createConfig() {
|
|
77
|
-
return {
|
|
78
|
-
instanceURL: '',
|
|
79
|
-
profiles: {},
|
|
80
|
-
defaultProfile: '',
|
|
81
|
-
activeProfile: '',
|
|
82
|
-
format: 'auto',
|
|
83
|
-
sources: {},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function loadConfig(overrides = {}) {
|
|
88
|
-
const cfg = createConfig();
|
|
89
|
-
|
|
90
|
-
loadFromFile(cfg, globalConfigPath(), 'global');
|
|
91
|
-
loadFromFile(cfg, localConfigPath(), 'local');
|
|
92
|
-
loadFromEnv(cfg);
|
|
93
|
-
|
|
94
|
-
if (overrides.instance) {
|
|
95
|
-
cfg.instanceURL = overrides.instance;
|
|
96
|
-
cfg.sources.instance_url = 'flag';
|
|
97
|
-
}
|
|
98
|
-
if (overrides.format) {
|
|
99
|
-
cfg.format = overrides.format;
|
|
100
|
-
cfg.sources.format = 'flag';
|
|
101
|
-
}
|
|
102
|
-
if (overrides.profile) {
|
|
103
|
-
cfg.activeProfile = overrides.profile;
|
|
104
|
-
if (cfg.profiles[overrides.profile] && cfg.profiles[overrides.profile].instance_url) {
|
|
105
|
-
cfg.instanceURL = cfg.profiles[overrides.profile].instance_url;
|
|
106
|
-
cfg.sources.instance_url = 'profile';
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return cfg;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function saveConfig(cfg) {
|
|
114
|
-
const dir = globalConfigDir();
|
|
115
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o750 });
|
|
116
|
-
const payload = {
|
|
117
|
-
instance_url: cfg.instanceURL || undefined,
|
|
118
|
-
profiles: Object.keys(cfg.profiles).length > 0 ? cfg.profiles : undefined,
|
|
119
|
-
default_profile: cfg.defaultProfile || undefined,
|
|
120
|
-
format: cfg.format || undefined,
|
|
121
|
-
};
|
|
122
|
-
fs.writeFileSync(globalConfigPath(), JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function saveLocalConfig(cfg) {
|
|
126
|
-
const dir = path.dirname(localConfigPath());
|
|
127
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o750 });
|
|
128
|
-
const payload = {
|
|
129
|
-
instance_url: cfg.instanceURL || undefined,
|
|
130
|
-
profiles: Object.keys(cfg.profiles).length > 0 ? cfg.profiles : undefined,
|
|
131
|
-
default_profile: cfg.defaultProfile || undefined,
|
|
132
|
-
format: cfg.format || undefined,
|
|
133
|
-
};
|
|
134
|
-
fs.writeFileSync(localConfigPath(), JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function getEffectiveInstance(cfg) {
|
|
138
|
-
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
139
|
-
if (name && cfg.profiles[name] && cfg.profiles[name].instance_url) {
|
|
140
|
-
return cfg.profiles[name].instance_url;
|
|
141
|
-
}
|
|
142
|
-
return cfg.instanceURL || '';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function getActiveProfile(cfg) {
|
|
146
|
-
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
147
|
-
if (!name) return null;
|
|
148
|
-
return cfg.profiles[name] || null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function setProfile(cfg, name, profile) {
|
|
152
|
-
cfg.profiles[name] = profile;
|
|
153
|
-
return saveConfig(cfg);
|
|
154
|
-
}
|
package/src/context.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// ServiceNow runtime context: current user, scope, update set
|
|
2
|
-
|
|
3
|
-
export async function getCurrentUser(sdk) {
|
|
4
|
-
const params = new URLSearchParams();
|
|
5
|
-
params.set('sysparm_query', 'user_name=javascript:gs.getUserName()');
|
|
6
|
-
params.set('sysparm_limit', '1');
|
|
7
|
-
params.set('sysparm_display_value', 'all');
|
|
8
|
-
params.set('sysparm_fields', 'sys_id,user_name,name');
|
|
9
|
-
const records = await sdk.list('sys_user', params);
|
|
10
|
-
if (records.length === 0) return null;
|
|
11
|
-
const r = records[0];
|
|
12
|
-
return {
|
|
13
|
-
sys_id: r.sys_id?.value || r.sys_id,
|
|
14
|
-
user_name: r.user_name?.display_value || r.user_name,
|
|
15
|
-
name: r.name?.display_value || r.name,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function getCurrentApplication(sdk, userSysID) {
|
|
20
|
-
const params = new URLSearchParams();
|
|
21
|
-
params.set('sysparm_query', `user=${userSysID}^name=apps.current_app`);
|
|
22
|
-
params.set('sysparm_limit', '1');
|
|
23
|
-
params.set('sysparm_fields', 'value');
|
|
24
|
-
const records = await sdk.list('sys_user_preference', params);
|
|
25
|
-
if (records.length === 0) return { scope: 'global' };
|
|
26
|
-
const val = records[0].value?.value || records[0].value;
|
|
27
|
-
if (!val) return { scope: 'global' };
|
|
28
|
-
|
|
29
|
-
// Try to resolve sys_id to scope name
|
|
30
|
-
try {
|
|
31
|
-
const app = await sdk.get('sys_scope', val);
|
|
32
|
-
if (app) return { scope: app.scope || 'global' };
|
|
33
|
-
} catch {
|
|
34
|
-
// fallback
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const app2 = await sdk.get('sys_app', val);
|
|
38
|
-
if (app2) return { scope: app2.scope || 'global' };
|
|
39
|
-
} catch {
|
|
40
|
-
// fallback
|
|
41
|
-
}
|
|
42
|
-
return { scope: val };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function getCurrentUpdateSet(sdk, userSysID) {
|
|
46
|
-
const params = new URLSearchParams();
|
|
47
|
-
params.set('sysparm_query', `user=${userSysID}^name=sys_update_set`);
|
|
48
|
-
params.set('sysparm_limit', '1');
|
|
49
|
-
params.set('sysparm_fields', 'value');
|
|
50
|
-
const records = await sdk.list('sys_user_preference', params);
|
|
51
|
-
if (records.length === 0) return null;
|
|
52
|
-
const val = records[0].value?.value || records[0].value;
|
|
53
|
-
if (!val || val === '-') return { name: 'Default', sys_id: '' };
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const us = await sdk.get('sys_update_set', val);
|
|
57
|
-
if (us) return { name: us.name || val, sys_id: val };
|
|
58
|
-
} catch {
|
|
59
|
-
// fallback
|
|
60
|
-
}
|
|
61
|
-
return { name: val, sys_id: val };
|
|
62
|
-
}
|
package/src/errors.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Structured error types with code, message, and optional hint.
|
|
2
|
-
|
|
3
|
-
export const CodeUsage = 'usage_error';
|
|
4
|
-
export const CodeNotFound = 'not_found';
|
|
5
|
-
export const CodeAuth = 'auth_error';
|
|
6
|
-
export const CodeForbidden = 'forbidden';
|
|
7
|
-
export const CodeRateLimit = 'rate_limited';
|
|
8
|
-
export const CodeNetwork = 'network_error';
|
|
9
|
-
export const CodeAPI = 'api_error';
|
|
10
|
-
export const CodeAmbiguous = 'ambiguous';
|
|
11
|
-
export const CodeEmptyResult = 'empty_result';
|
|
12
|
-
|
|
13
|
-
export class AppError extends Error {
|
|
14
|
-
constructor(code, message, hint = '', status = 0, cause = null) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.name = 'AppError';
|
|
17
|
-
this.code = code;
|
|
18
|
-
this.hint = hint;
|
|
19
|
-
this.status = status;
|
|
20
|
-
this.cause = cause;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
toString() {
|
|
24
|
-
if (this.hint) {
|
|
25
|
-
return `${this.code}: ${this.message}\nHint: ${this.hint}`;
|
|
26
|
-
}
|
|
27
|
-
return `${this.code}: ${this.message}`;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function errUsage(msg) {
|
|
32
|
-
return new AppError(CodeUsage, msg);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function errUsageHint(msg, hint) {
|
|
36
|
-
return new AppError(CodeUsage, msg, hint);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function errNotFound(resource, identifier) {
|
|
40
|
-
return new AppError(
|
|
41
|
-
CodeNotFound,
|
|
42
|
-
`${resource} not found: ${identifier}`,
|
|
43
|
-
`Check the ${resource} exists and you have access to it.`
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function errNotFoundHint(resource, identifier, hint) {
|
|
48
|
-
return new AppError(CodeNotFound, `${resource} not found: ${identifier}`, hint);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function errAuth(msg) {
|
|
52
|
-
return new AppError(CodeAuth, msg, 'Run: jsn auth login');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function errForbidden(msg) {
|
|
56
|
-
return new AppError(CodeForbidden, msg, '', 403);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function errRateLimit(retryAfter) {
|
|
60
|
-
return new AppError(
|
|
61
|
-
CodeRateLimit,
|
|
62
|
-
`Rate limited. Retry after ${retryAfter} seconds.`,
|
|
63
|
-
'Wait before retrying, or use pagination for large queries.'
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function errNetwork(cause) {
|
|
68
|
-
return new AppError(
|
|
69
|
-
CodeNetwork,
|
|
70
|
-
`Network error: ${cause.message || cause}`,
|
|
71
|
-
'Check your internet connection and instance URL.',
|
|
72
|
-
0,
|
|
73
|
-
cause
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function errAPI(status, msg) {
|
|
78
|
-
let hint = 'Check the API documentation for this endpoint.';
|
|
79
|
-
if (status >= 500) {
|
|
80
|
-
hint = 'The ServiceNow instance may be experiencing issues. Try again later.';
|
|
81
|
-
}
|
|
82
|
-
return new AppError(CodeAPI, `API error (status ${status}): ${msg}`, hint, status);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function errAmbiguous(resource, matches) {
|
|
86
|
-
return new AppError(
|
|
87
|
-
CodeAmbiguous,
|
|
88
|
-
`Multiple ${resource} found matching your query`,
|
|
89
|
-
`Did you mean one of: ${matches.join(', ')}?`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function asError(err) {
|
|
94
|
-
if (!err) return null;
|
|
95
|
-
if (err instanceof AppError) return err;
|
|
96
|
-
return new AppError('unknown', err.message || String(err));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function isErrorCode(err, code) {
|
|
100
|
-
return err instanceof AppError && err.code === code;
|
|
101
|
-
}
|
package/src/helpers.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Shared helper utilities
|
|
2
|
-
|
|
3
|
-
export function getStringField(record, field) {
|
|
4
|
-
if (!record || typeof record !== 'object') return '';
|
|
5
|
-
const val = record[field];
|
|
6
|
-
if (val == null) return '';
|
|
7
|
-
if (typeof val === 'string') return val;
|
|
8
|
-
if (typeof val === 'object') {
|
|
9
|
-
if (val.display_value != null) return String(val.display_value);
|
|
10
|
-
if (val.value != null) return String(val.value);
|
|
11
|
-
}
|
|
12
|
-
return String(val);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function formatRecordForDisplay(record, columns) {
|
|
16
|
-
const result = {};
|
|
17
|
-
|
|
18
|
-
function extractValue(val) {
|
|
19
|
-
if (val == null) return '';
|
|
20
|
-
if (typeof val === 'string') return val;
|
|
21
|
-
if (typeof val === 'object') {
|
|
22
|
-
if (val.display_value != null && val.display_value !== '') return String(val.display_value);
|
|
23
|
-
if (val.value != null) return String(val.value);
|
|
24
|
-
}
|
|
25
|
-
return String(val);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (record.sys_id != null) {
|
|
29
|
-
result.sys_id = extractValue(record.sys_id);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
for (const col of columns) {
|
|
33
|
-
if (record[col] != null) {
|
|
34
|
-
result[col] = extractValue(record[col]);
|
|
35
|
-
} else {
|
|
36
|
-
result[col] = '';
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function truncateString(str, maxLen) {
|
|
43
|
-
if (!str || str.length <= maxLen) return str;
|
|
44
|
-
return str.slice(0, maxLen - 3) + '...';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function isHexString(str) {
|
|
48
|
-
return /^[0-9a-fA-F]+$/.test(str);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function extractProfileName(instanceURL) {
|
|
52
|
-
let name = instanceURL.replace(/^https?:\/\//, '');
|
|
53
|
-
name = name.replace(/\.service-now\.com$/, '');
|
|
54
|
-
name = name.replace(/\.servicenowservices\.com$/, '');
|
|
55
|
-
return name;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function buildQuerySuffix(query) {
|
|
59
|
-
return query ? ` --query "${query}"` : '';
|
|
60
|
-
}
|