@jacebenson/jsn 4.3.12 → 4.3.13
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 +1 -1
- package/src/cli-adapter.js +12 -4
- package/src/commands/_generic.js +13 -9
- package/src/commands/_ticket.js +14 -11
- package/src/commands/groups.js +11 -8
- package/src/commands/records.js +11 -16
- package/src/commands/requests.js +1 -1
- package/src/commands/tickets.js +14 -6
- package/src/commands/users.js +11 -8
- package/src/helpers.js +44 -0
package/package.json
CHANGED
package/src/cli-adapter.js
CHANGED
|
@@ -3,6 +3,14 @@ import process from 'node:process';
|
|
|
3
3
|
|
|
4
4
|
const camel = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
5
5
|
const names = (x) => typeof x === 'string' ? [x] : (x || []);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A repeatable option may be declared either way: `type: 'array'` (this
|
|
9
|
+
* adapter's historic spelling) or `array: true` (the yargs spelling most
|
|
10
|
+
* contributors reach for). Both must accumulate; honoring only the first
|
|
11
|
+
* silently kept the LAST value of `-f a=1 -f b=2` and dropped the rest.
|
|
12
|
+
*/
|
|
13
|
+
const isArrayOption = (spec) => spec?.type === 'array' || spec?.array === true;
|
|
6
14
|
const commandName = (command) => command.trim().split(/\s+/)[0];
|
|
7
15
|
const positionSpec = (command) => [...command.matchAll(/([<[])([^>\]]+)[>\]]/g)].map((m) => ({ name: m[2], required: m[1] === '<' }));
|
|
8
16
|
|
|
@@ -189,7 +197,7 @@ export class LocalCLI {
|
|
|
189
197
|
_assign(argv, key, value, spec, opts = {}) {
|
|
190
198
|
const validate = opts.validate !== false;
|
|
191
199
|
const actual = spec?.name || key;
|
|
192
|
-
const values = spec
|
|
200
|
+
const values = isArrayOption(spec) ? (Array.isArray(value) ? value : [value]) : [value];
|
|
193
201
|
if (validate && spec?.choices && values.some((item) => !spec.choices.includes(item))) {
|
|
194
202
|
this._fail(`Invalid value for --${actual}: ${value}. Choose from: ${spec.choices.join(', ')}`);
|
|
195
203
|
return false;
|
|
@@ -201,7 +209,7 @@ export class LocalCLI {
|
|
|
201
209
|
const converted = spec?.type === 'boolean'
|
|
202
210
|
? (value === 'true' ? true : value === 'false' ? false : value)
|
|
203
211
|
: spec?.type === 'number' ? Number(value) : value;
|
|
204
|
-
if (spec
|
|
212
|
+
if (isArrayOption(spec)) argv[actual] = [...(argv[actual] || []), ...values];
|
|
205
213
|
else argv[actual] = converted;
|
|
206
214
|
argv[camel(actual)] = argv[actual];
|
|
207
215
|
for (const alias of names(spec?.alias)) argv[alias] = argv[actual];
|
|
@@ -210,8 +218,8 @@ export class LocalCLI {
|
|
|
210
218
|
const options = this._optionConfig(path);
|
|
211
219
|
const defs = {};
|
|
212
220
|
for (const [key, spec] of options) defs[key] = {
|
|
213
|
-
type: spec.type === 'number' || spec
|
|
214
|
-
multiple: spec
|
|
221
|
+
type: spec.type === 'number' || isArrayOption(spec) ? 'string' : (spec.type || 'boolean'),
|
|
222
|
+
multiple: isArrayOption(spec),
|
|
215
223
|
};
|
|
216
224
|
args = args.map((arg) => {
|
|
217
225
|
if (/^-[^-]=/.test(arg)) return `--${arg.slice(1)}`;
|
package/src/commands/_generic.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generic table-based CRUD command builder
|
|
2
2
|
|
|
3
|
-
import { formatRecordForDisplay, getStringField,
|
|
3
|
+
import { formatRecordForDisplay, getStringField, buildRecordPayload, confirmDelete, interactiveList, canPrompt } from '../helpers.js';
|
|
4
4
|
import { resolveRecord, unwrapSysId } from '../resolve-record.js';
|
|
5
5
|
import { getCurrentUser, getCurrentApplication } from '../context.js';
|
|
6
6
|
import { FormatAuto } from '../output.js';
|
|
@@ -108,7 +108,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
108
108
|
if (!readOnly) {
|
|
109
109
|
crumbs.unshift(
|
|
110
110
|
{ action: 'delete', cmd: `jsn ${name} delete ${id}`, description: `Delete this ${singular}` },
|
|
111
|
-
{ action: 'update', cmd: `jsn ${name} update ${id}
|
|
111
|
+
{ action: 'update', cmd: `jsn ${name} update ${id} -f name=value`, description: `Update this ${singular}` }
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
return crumbs;
|
|
@@ -152,7 +152,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
152
152
|
app.ok(record, {
|
|
153
153
|
summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${recordName}`,
|
|
154
154
|
breadcrumbs: [
|
|
155
|
-
...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${recordName}
|
|
155
|
+
...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${recordName} -f name=value`, description: `Update this ${singular}` }]),
|
|
156
156
|
{ action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
|
|
157
157
|
],
|
|
158
158
|
});
|
|
@@ -215,10 +215,12 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
215
215
|
command: 'create',
|
|
216
216
|
describe: `Create a new ${singular}`,
|
|
217
217
|
builder: (y) => y
|
|
218
|
-
.option('data', { type: 'string', describe: 'JSON fields
|
|
219
|
-
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
218
|
+
.option('data', { type: 'string', describe: 'JSON fields' })
|
|
219
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
220
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
221
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
220
222
|
handler: wrap(async (argv, app) => {
|
|
221
|
-
const recordData =
|
|
223
|
+
const recordData = buildRecordPayload(argv);
|
|
222
224
|
const record = await app.sdk.create(table, recordData);
|
|
223
225
|
app.ok(record, {
|
|
224
226
|
summary: `Created ${singular}`,
|
|
@@ -232,8 +234,10 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
232
234
|
command: 'update <identifier>',
|
|
233
235
|
describe: `Update ${vowelArticle(singular)} ${singular}`,
|
|
234
236
|
builder: (y) => y
|
|
235
|
-
.option('data', { type: 'string', describe: 'JSON fields
|
|
236
|
-
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
237
|
+
.option('data', { type: 'string', describe: 'JSON fields' })
|
|
238
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
239
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
240
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
237
241
|
handler: wrap(async (argv, app) => {
|
|
238
242
|
const id = argv.identifier;
|
|
239
243
|
const record = await resolveRecord(app.sdk, { table, identifier: id, matchField: 'name', resource: singular });
|
|
@@ -242,7 +246,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
const sysID = unwrapSysId(record);
|
|
245
|
-
const recordData =
|
|
249
|
+
const recordData = buildRecordPayload(argv);
|
|
246
250
|
const updated = await app.sdk.update(table, sysID, recordData);
|
|
247
251
|
app.ok(updated, { summary: `Updated ${singular} ${id}` });
|
|
248
252
|
}),
|
package/src/commands/_ticket.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Generic command builder for CRUD operations on a ServiceNow table
|
|
2
2
|
// Used by incidents, changes, requests, tasks, and most dev subcommands
|
|
3
3
|
|
|
4
|
-
import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam, confirmDelete, interactiveList, canPrompt } from '../helpers.js';
|
|
4
|
+
import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam, confirmDelete, interactiveList, canPrompt, buildRecordPayload } from '../helpers.js';
|
|
5
5
|
import { resolveRecord, resolveSysId } from '../resolve-record.js';
|
|
6
6
|
import { FormatAuto } from '../output.js';
|
|
7
7
|
import { declareCapabilities } from '../capabilities.js';
|
|
@@ -73,7 +73,7 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
73
73
|
app.ok(record, {
|
|
74
74
|
summary: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} ${number}`,
|
|
75
75
|
breadcrumbs: [
|
|
76
|
-
{ action: 'update', cmd: `jsn ${alias} update ${number}
|
|
76
|
+
{ action: 'update', cmd: `jsn ${alias} update ${number} -f state=2`, description: `Update this ${displayName}` },
|
|
77
77
|
{ action: 'list', cmd: `jsn ${alias} list`, description: `Back to all ${table}` },
|
|
78
78
|
],
|
|
79
79
|
});
|
|
@@ -139,7 +139,7 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
139
139
|
app.ok(record, {
|
|
140
140
|
summary: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} ${number}`,
|
|
141
141
|
breadcrumbs: [
|
|
142
|
-
{ action: 'update', cmd: `jsn ${alias} update ${number}
|
|
142
|
+
{ action: 'update', cmd: `jsn ${alias} update ${number} -f state=2`, description: `Update this ${displayName}` },
|
|
143
143
|
{ action: 'list', cmd: `jsn ${alias} list`, description: `Back to all ${table}` },
|
|
144
144
|
],
|
|
145
145
|
});
|
|
@@ -151,16 +151,16 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
151
151
|
builder: (y) => y
|
|
152
152
|
.option('description', { alias: 'd', type: 'string', describe: 'Short description' })
|
|
153
153
|
.option('priority', { type: 'string', describe: 'Priority (1-5)' })
|
|
154
|
-
.option('data', { type: 'string', describe: 'JSON data for additional fields' })
|
|
154
|
+
.option('data', { type: 'string', describe: 'JSON data for additional fields' })
|
|
155
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
156
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
157
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
155
158
|
handler: wrap(async (argv, app) => {
|
|
156
|
-
const recordData = {};
|
|
157
|
-
if (argv.data) {
|
|
158
|
-
Object.assign(recordData, JSON.parse(argv.data));
|
|
159
|
-
}
|
|
159
|
+
const recordData = buildRecordPayload(argv, { required: false });
|
|
160
160
|
if (argv.description) recordData.short_description = argv.description;
|
|
161
161
|
if (argv.priority) recordData.priority = argv.priority;
|
|
162
162
|
if (!recordData.short_description) {
|
|
163
|
-
throw new Error('short_description is required (use --description or --data)');
|
|
163
|
+
throw new Error('short_description is required (use --description, -f short_description=..., or --data)');
|
|
164
164
|
}
|
|
165
165
|
const record = await app.sdk.create(table, recordData);
|
|
166
166
|
app.ok(record, {
|
|
@@ -175,10 +175,13 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
175
175
|
command: 'update <number>',
|
|
176
176
|
describe: `Update a ${displayName}`,
|
|
177
177
|
builder: (y) => y
|
|
178
|
-
.option('data', { type: 'string',
|
|
178
|
+
.option('data', { type: 'string', describe: 'JSON data to update' })
|
|
179
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
180
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
181
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
179
182
|
handler: wrap(async (argv, app) => {
|
|
180
183
|
const number = argv.number;
|
|
181
|
-
const recordData =
|
|
184
|
+
const recordData = buildRecordPayload(argv);
|
|
182
185
|
const sysID = await resolveSysId(app.sdk, { table, identifier: number, matchField: 'number', resource: displayName });
|
|
183
186
|
const updated = await app.sdk.update(table, sysID, recordData);
|
|
184
187
|
app.ok(updated, {
|
package/src/commands/groups.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, getStringField, interactiveList, resolveFieldsParam, assertSafeExactMatch } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList, resolveFieldsParam, assertSafeExactMatch, buildRecordPayload } from '../helpers.js';
|
|
2
2
|
import { declareCapabilities } from '../capabilities.js';
|
|
3
3
|
|
|
4
4
|
declareCapabilities('groups', { mutationSubcommands: ['create', 'update', 'delete'] });
|
|
@@ -94,12 +94,12 @@ export function groupsCmd(wrap) {
|
|
|
94
94
|
builder: (y) => y
|
|
95
95
|
.option('name', { alias: 'n', type: 'string', describe: 'Group name' })
|
|
96
96
|
.option('manager', { type: 'string', describe: 'Manager name or sys_id' })
|
|
97
|
-
.option('data', { type: 'string', describe: 'JSON data for additional fields' })
|
|
97
|
+
.option('data', { type: 'string', describe: 'JSON data for additional fields' })
|
|
98
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
99
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
100
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
98
101
|
handler: wrap(async (argv, app) => {
|
|
99
|
-
const recordData = {};
|
|
100
|
-
if (argv.data) {
|
|
101
|
-
Object.assign(recordData, JSON.parse(argv.data));
|
|
102
|
-
}
|
|
102
|
+
const recordData = buildRecordPayload(argv, { required: false });
|
|
103
103
|
if (argv.name) recordData.name = argv.name;
|
|
104
104
|
if (argv.manager) recordData.manager = argv.manager;
|
|
105
105
|
if (!recordData.name) {
|
|
@@ -118,11 +118,14 @@ export function groupsCmd(wrap) {
|
|
|
118
118
|
command: 'update <name>',
|
|
119
119
|
describe: 'Update a group by name or sys_id',
|
|
120
120
|
builder: (y) => y
|
|
121
|
-
.option('data', { type: 'string',
|
|
121
|
+
.option('data', { type: 'string', describe: 'JSON data to update' })
|
|
122
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
123
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
124
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
122
125
|
handler: wrap(async (argv, app) => {
|
|
123
126
|
const id = argv.name;
|
|
124
127
|
assertSafeExactMatch(id);
|
|
125
|
-
const recordData =
|
|
128
|
+
const recordData = buildRecordPayload(argv);
|
|
126
129
|
const isSysID = id.length === 32 && /^[0-9a-fA-F]+$/.test(id);
|
|
127
130
|
const params = new URLSearchParams();
|
|
128
131
|
params.set('sysparm_query', isSysID ? `sys_id=${id}` : `name=${id}`);
|
package/src/commands/records.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { formatCommandList, formatRecordForDisplay, buildQuerySuffix,
|
|
3
|
+
import { formatCommandList, formatRecordForDisplay, buildQuerySuffix, buildRecordPayload, getStringField, interactiveList, resolveFieldsParam, checkDerivedFields, confirmDelete, assertSafeExactMatch, verifyWriteBack } from '../helpers.js';
|
|
4
4
|
import { declareCapabilities } from '../capabilities.js';
|
|
5
5
|
|
|
6
6
|
declareCapabilities('records', {
|
|
@@ -142,7 +142,7 @@ export function recordsCmd(wrap) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const breadcrumbs = [
|
|
145
|
-
{ action: 'create', cmd: `jsn records create --table ${table}
|
|
145
|
+
{ action: 'create', cmd: `jsn records create --table ${table} -f name=value`, description: 'Create a new record' },
|
|
146
146
|
{ action: 'filter', cmd: `jsn records list --table ${table} --query "priority=1"`, description: 'Filter: priority 1 only' },
|
|
147
147
|
{ action: 'columns', cmd: `jsn columns --table ${table}`, description: 'View available columns' },
|
|
148
148
|
];
|
|
@@ -221,12 +221,13 @@ export function recordsCmd(wrap) {
|
|
|
221
221
|
describe: 'Create a new record',
|
|
222
222
|
builder: (y) => y
|
|
223
223
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
224
|
-
.option('data', { type: 'string', describe: 'JSON fields
|
|
224
|
+
.option('data', { type: 'string', describe: 'JSON fields' })
|
|
225
225
|
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
226
226
|
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin (pipe-friendly)' })
|
|
227
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' })
|
|
227
228
|
.option('strict', { type: 'boolean', default: false, describe: 'Exit non-zero if any supplied field was not persisted (write-back check)' }),
|
|
228
229
|
handler: wrap(async (argv, app) => {
|
|
229
|
-
const recordData =
|
|
230
|
+
const recordData = buildRecordPayload(argv);
|
|
230
231
|
const warnings = checkDerivedFields(argv.table, recordData);
|
|
231
232
|
for (const w of warnings) {
|
|
232
233
|
process.stderr.write(`⚠ ${w.hint}\n`);
|
|
@@ -249,12 +250,13 @@ export function recordsCmd(wrap) {
|
|
|
249
250
|
builder: (y) => y
|
|
250
251
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
251
252
|
.option('sys-id', { type: 'string', demandOption: true, describe: 'Record sys_id' })
|
|
252
|
-
.option('data', { type: 'string', describe: 'JSON fields
|
|
253
|
+
.option('data', { type: 'string', describe: 'JSON fields' })
|
|
253
254
|
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
254
255
|
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin (pipe-friendly)' })
|
|
256
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' })
|
|
255
257
|
.option('strict', { type: 'boolean', default: false, describe: 'Exit non-zero if any supplied field was not persisted (write-back check)' }),
|
|
256
258
|
handler: wrap(async (argv, app) => {
|
|
257
|
-
const recordData =
|
|
259
|
+
const recordData = buildRecordPayload(argv);
|
|
258
260
|
const warnings = checkDerivedFields(argv.table, recordData);
|
|
259
261
|
for (const w of warnings) {
|
|
260
262
|
process.stderr.write(`⚠ ${w.hint}\n`);
|
|
@@ -401,22 +403,15 @@ export function recordsCmd(wrap) {
|
|
|
401
403
|
builder: (y) => y
|
|
402
404
|
.option('table', { type: 'string', demandOption: true, describe: 'Table name' })
|
|
403
405
|
.option('query', { type: 'string', demandOption: true, describe: 'Encoded query selecting the records to update (e.g. "priority=1^state=1")' })
|
|
404
|
-
.option('set', { type: 'string',
|
|
406
|
+
.option('set', { type: 'string', describe: 'JSON object of fields to set' })
|
|
407
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' })
|
|
405
408
|
.option('dry-run', { type: 'boolean', default: true, describe: 'Preview the count + a sample without mutating (default)' })
|
|
406
409
|
.option('execute', { type: 'boolean', default: false, describe: 'Perform the update after confirmation' })
|
|
407
410
|
.option('limit', { type: 'number', default: 200, describe: 'Max records to update when executing' })
|
|
408
411
|
.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation (with --execute)' }),
|
|
409
412
|
handler: wrap(async (argv, app) => {
|
|
410
413
|
app.requireInstance();
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
set = JSON.parse(argv.set);
|
|
414
|
-
} catch (e) {
|
|
415
|
-
throw new Error(`Invalid --set JSON: ${e.message}`, { cause: e });
|
|
416
|
-
}
|
|
417
|
-
if (!set || typeof set !== 'object' || Array.isArray(set)) {
|
|
418
|
-
throw new Error('--set must be a JSON object of field=value pairs');
|
|
419
|
-
}
|
|
414
|
+
const set = buildRecordPayload({ ...argv, data: argv.set }, { required: true });
|
|
420
415
|
if (!argv['dry-run'] && !argv.execute) {
|
|
421
416
|
argv.execute = true; // explicit --no-dry-run implies execute intent
|
|
422
417
|
}
|
package/src/commands/requests.js
CHANGED
|
@@ -53,7 +53,7 @@ async function enrichedShowHandler(argv, app) {
|
|
|
53
53
|
app.ok(record, {
|
|
54
54
|
summary: `Request ${number}`,
|
|
55
55
|
breadcrumbs: [
|
|
56
|
-
{ action: 'update', cmd: `jsn req update ${number}
|
|
56
|
+
{ action: 'update', cmd: `jsn req update ${number} -f state=2`, description: 'Update this request' },
|
|
57
57
|
{ action: 'list', cmd: `jsn req list`, description: 'Back to all requests' },
|
|
58
58
|
],
|
|
59
59
|
});
|
package/src/commands/tickets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, getStringField, buildQuerySuffix, resolveFieldsParam } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, buildQuerySuffix, resolveFieldsParam, buildRecordPayload } from '../helpers.js';
|
|
2
2
|
import { resolveRecord, resolveSysId } from '../resolve-record.js';
|
|
3
3
|
import { declareCapabilities } from '../capabilities.js';
|
|
4
4
|
|
|
@@ -33,7 +33,7 @@ export function ticketsCmd(wrap) {
|
|
|
33
33
|
const records = await app.sdk.list('task', params);
|
|
34
34
|
const displayRecords = fields ? records.map(r => formatRecordForDisplay(r, columns)) : records;
|
|
35
35
|
const breadcrumbs = [
|
|
36
|
-
{ action: 'create', cmd: 'jsn tickets create
|
|
36
|
+
{ action: 'create', cmd: 'jsn tickets create -f short_description=New ticket', description: 'Create a new ticket' },
|
|
37
37
|
];
|
|
38
38
|
if (records.length === argv.limit) {
|
|
39
39
|
breadcrumbs.push({
|
|
@@ -71,9 +71,13 @@ export function ticketsCmd(wrap) {
|
|
|
71
71
|
.command({
|
|
72
72
|
command: 'create',
|
|
73
73
|
describe: 'Create a new ticket',
|
|
74
|
-
builder: (y) => y
|
|
74
|
+
builder: (y) => y
|
|
75
|
+
.option('data', { type: 'string', describe: 'JSON fields' })
|
|
76
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
77
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
78
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
75
79
|
handler: wrap(async (argv, app) => {
|
|
76
|
-
const recordData =
|
|
80
|
+
const recordData = buildRecordPayload(argv);
|
|
77
81
|
const record = await app.sdk.create('task', recordData);
|
|
78
82
|
app.ok(record, { summary: `Created ticket ${getStringField(record, 'number')}` });
|
|
79
83
|
}),
|
|
@@ -81,9 +85,13 @@ export function ticketsCmd(wrap) {
|
|
|
81
85
|
.command({
|
|
82
86
|
command: 'update <number>',
|
|
83
87
|
describe: 'Update a ticket',
|
|
84
|
-
builder: (y) => y
|
|
88
|
+
builder: (y) => y
|
|
89
|
+
.option('data', { type: 'string', describe: 'JSON fields' })
|
|
90
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
91
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
92
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
85
93
|
handler: wrap(async (argv, app) => {
|
|
86
|
-
const recordData =
|
|
94
|
+
const recordData = buildRecordPayload(argv);
|
|
87
95
|
const sysID = await resolveSysId(app.sdk, { table: 'task', identifier: argv.number, matchField: 'number', resource: 'Ticket' });
|
|
88
96
|
const updated = await app.sdk.update('task', sysID, recordData);
|
|
89
97
|
app.ok(updated, { summary: `Updated ticket ${argv.number}` });
|
package/src/commands/users.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, getStringField, interactiveList, resolveFieldsParam } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, getStringField, interactiveList, resolveFieldsParam, buildRecordPayload } from '../helpers.js';
|
|
2
2
|
import { resolveRecord, resolveSysId } from '../resolve-record.js';
|
|
3
3
|
import { declareCapabilities } from '../capabilities.js';
|
|
4
4
|
|
|
@@ -72,12 +72,12 @@ export function usersCmd(wrap) {
|
|
|
72
72
|
.option('username', { alias: 'u', type: 'string', describe: 'Username (e.g. john.doe)' })
|
|
73
73
|
.option('name', { alias: 'n', type: 'string', describe: 'Full name (e.g. John Doe)' })
|
|
74
74
|
.option('email', { alias: 'e', type: 'string', describe: 'Email address' })
|
|
75
|
-
.option('data', { type: 'string', describe: 'JSON data for additional fields' })
|
|
75
|
+
.option('data', { type: 'string', describe: 'JSON data for additional fields' })
|
|
76
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
77
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
78
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
76
79
|
handler: wrap(async (argv, app) => {
|
|
77
|
-
const recordData = {};
|
|
78
|
-
if (argv.data) {
|
|
79
|
-
Object.assign(recordData, JSON.parse(argv.data));
|
|
80
|
-
}
|
|
80
|
+
const recordData = buildRecordPayload(argv, { required: false });
|
|
81
81
|
if (argv.username) recordData.user_name = argv.username;
|
|
82
82
|
if (argv.name) recordData.name = argv.name;
|
|
83
83
|
if (argv.email) recordData.email = argv.email;
|
|
@@ -97,10 +97,13 @@ export function usersCmd(wrap) {
|
|
|
97
97
|
command: 'update <identifier>',
|
|
98
98
|
describe: 'Update a user by user_name or sys_id',
|
|
99
99
|
builder: (y) => y
|
|
100
|
-
.option('data', { type: 'string',
|
|
100
|
+
.option('data', { type: 'string', describe: 'JSON data to update' })
|
|
101
|
+
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
|
|
102
|
+
.option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
|
|
103
|
+
.option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
|
|
101
104
|
handler: wrap(async (argv, app) => {
|
|
102
105
|
const id = argv.identifier;
|
|
103
|
-
const recordData =
|
|
106
|
+
const recordData = buildRecordPayload(argv);
|
|
104
107
|
const sysID = await resolveSysId(app.sdk, { table: 'sys_user', identifier: id, matchField: 'user_name', resource: 'User' });
|
|
105
108
|
const updated = await app.sdk.update('sys_user', sysID, recordData);
|
|
106
109
|
app.ok(updated, {
|
package/src/helpers.js
CHANGED
|
@@ -522,6 +522,50 @@ export function parseDataArg(argv) {
|
|
|
522
522
|
}
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Parse repeatable -f/--field name=value options.
|
|
527
|
+
* Values remain strings, including empty values and values containing '='.
|
|
528
|
+
*/
|
|
529
|
+
export function parseFieldPairs(argv = {}) {
|
|
530
|
+
const raw = argv.field == null ? [] : (Array.isArray(argv.field) ? argv.field : [argv.field]);
|
|
531
|
+
const result = {};
|
|
532
|
+
for (const token of raw) {
|
|
533
|
+
const text = String(token);
|
|
534
|
+
const equals = text.indexOf('=');
|
|
535
|
+
if (equals < 0) throw new Error(`-f expects field=value, got "${text}"`);
|
|
536
|
+
const name = text.slice(0, equals);
|
|
537
|
+
let value = text.slice(equals + 1);
|
|
538
|
+
if (!name || /\\s/.test(name) || !/^[A-Za-z0-9_.]+$/.test(name)) {
|
|
539
|
+
throw new Error(`-f expects a valid field name, got "${name}"`);
|
|
540
|
+
}
|
|
541
|
+
if (value.startsWith('@@')) {
|
|
542
|
+
value = value.slice(1);
|
|
543
|
+
} else if (value.startsWith('@')) {
|
|
544
|
+
value = fs.readFileSync(value.slice(1), 'utf-8');
|
|
545
|
+
if (value.charCodeAt(0) === 0xFEFF) value = value.slice(1);
|
|
546
|
+
}
|
|
547
|
+
result[name] = value;
|
|
548
|
+
}
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Build a record mutation payload from JSON input and/or field pairs.
|
|
554
|
+
*/
|
|
555
|
+
export function buildRecordPayload(argv = {}, { required = true } = {}) {
|
|
556
|
+
const hasData = Boolean(argv.data || argv['data-file'] || argv['data-stdin']);
|
|
557
|
+
const hasFields = argv.field != null && (Array.isArray(argv.field) ? argv.field.length > 0 : true);
|
|
558
|
+
if (!hasData && !hasFields) {
|
|
559
|
+
if (required) throw new Error('Provide fields with -f name=value, or a JSON payload with --data/--data-file/--data-stdin');
|
|
560
|
+
return {};
|
|
561
|
+
}
|
|
562
|
+
const payload = hasData ? parseDataArg(argv) : {};
|
|
563
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
564
|
+
throw new Error('Record payload must be a JSON object');
|
|
565
|
+
}
|
|
566
|
+
return { ...payload, ...parseFieldPairs(argv) };
|
|
567
|
+
}
|
|
568
|
+
|
|
525
569
|
/**
|
|
526
570
|
* Translate human-readable type names to ServiceNow item_option_new type IDs.
|
|
527
571
|
* Maps common names like "date", "select", "multilinetext" to their integer IDs.
|