Skip to content

Commit 5cd5eee

Browse files
author
Matt Willer
authored
Catch bulk input errors with converted delegate command (#134)
1 parent efa33ed commit 5cd5eee

File tree

36 files changed

+640
-305
lines changed

36 files changed

+640
-305
lines changed

src/box-command.js

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ class BoxCommand extends Command {
254254
this.constructor.args = originalArgs;
255255
this.constructor.flags = originalFlags;
256256
this.bulkOutputList = [];
257+
this.bulkErrors = [];
257258
this._singleRun = this.run;
258259
this.run = this.bulkOutputRun;
259260
}
@@ -379,8 +380,10 @@ class BoxCommand extends Command {
379380
bulkCalls = bulkCalls.map(args => args.filter(o => o !== undefined));
380381
DEBUG.execute('Read %d entries from bulk file %s', bulkCalls.length, this.flags['bulk-file-path']);
381382

383+
let bulkEntryIndex = 0;
382384
for (let bulkData of bulkCalls) {
383385
this.argv = [];
386+
bulkEntryIndex += 1;
384387

385388
// For each possible arg, find the correct value between bulk input
386389
// and values given on the command line
@@ -411,13 +414,36 @@ class BoxCommand extends Command {
411414
DEBUG.execute('Executing in bulk mode argv: %O', this.argv);
412415
// @TODO(2018-08-29): Convert this to a promise queue to improve performance
413416
/* eslint-disable no-await-in-loop */
414-
await this._singleRun();
417+
try {
418+
await this._singleRun();
419+
} catch (err) {
420+
// In bulk mode, we don't want to write directly to console and kill the command
421+
// Instead, we should buffer the error output so subsequent commands might be able to succeed
422+
DEBUG.execute('Caught error from bulk input entry %d', bulkEntryIndex);
423+
this.bulkErrors.push({index: bulkEntryIndex, data: bulkData, error: err});
424+
}
415425
/* eslint-enable no-await-in-loop */
416426
}
417427

418428
this.isBulk = false;
419429
DEBUG.execute('Leaving bulk mode and writing final output');
420430
await this.output(this.bulkOutputList);
431+
let numErrors = this.bulkErrors.length;
432+
if (numErrors > 0) {
433+
this.info(chalk`{redBright ${numErrors} entr${numErrors > 1 ? 'ies' : 'y'} failed!}`);
434+
this.bulkErrors.forEach(errorInfo => {
435+
this.info(chalk`{dim ----------}`);
436+
let entryData = errorInfo.data
437+
.map(o => ` ${o.fieldKey}=${o.value}`)
438+
.join(os.EOL);
439+
this.info(chalk`{redBright Entry ${errorInfo.index} (${os.EOL + entryData + os.EOL}) failed with error:}`);
440+
let err = errorInfo.error;
441+
let errMsg = chalk`{redBright ${this.flags && this.flags.verbose ? err.stack : err.message}${os.EOL}}`;
442+
this.info(errMsg);
443+
});
444+
} else {
445+
this.info(chalk`{green All bulk input entries processed successfully.}`);
446+
}
421447
}
422448

423449
/**
@@ -773,21 +799,22 @@ class BoxCommand extends Command {
773799
/* eslint-disable no-shadow,no-catch-shadow */
774800
} catch (err) {
775801
// The oclif default catch handler rethrows most errors; handle those here
776-
DEBUG.execute('Handling re-thrown error in handler');
802+
DEBUG.execute('Handling re-thrown error in base command handler');
777803

778804
if (err.code === 'EEXIT') {
779805
// oclif throws this when it handled the error itself and wants to exit, so just let it do that
780806
DEBUG.execute('Got EEXIT code, exiting immediately');
781807
return;
782808
}
783809

784-
let errorMsg = this.flags && this.flags.verbose ? err.stack : err.message;
810+
let errorMsg = chalk`{redBright ${this.flags && this.flags.verbose ? err.stack : err.message}${os.EOL}}`;
785811

786812
// Write the error message but let the process exit gracefully with error code so stderr gets written out
787813
// @NOTE: Exiting the process in the callback enables tests to mock out stderr and run to completion!
788814
/* eslint-disable no-process-exit,unicorn/no-process-exit */
789-
process.stderr.write(chalk`{redBright ${errorMsg}${os.EOL}}`, 'utf8', () => process.exit(2));
815+
process.stderr.write(errorMsg, 'utf8', () => process.exit(2));
790816
/* eslint-enable no-process-exit,unicorn/no-process-exit */
817+
791818
}
792819

793820
}
@@ -883,12 +910,13 @@ class BoxCommand extends Command {
883910
}
884911

885912
/**
886-
* Converts time interval shorthands like 5w, -3d, etc to timestamps. It also ensures any timestamp
887-
* passed in is properly formatted for API calls
913+
* Converts time interval shorthand like 5w, -3d, etc to timestamps. It also ensures any timestamp
914+
* passed in is properly formatted for API calls.
915+
*
888916
* @param {string} time The command lint input string for the datetime
889-
* @returns {string} The full RFC3339-formatted datetime string
917+
* @returns {string} The full RFC3339-formatted datetime string in UTC
890918
*/
891-
getDateFromString(time) {
919+
static normalizeDateString(time) {
892920
// Attempt to parse date as timestamp or string
893921
let newDate = time.match(/^\d+$/u) ? dateTime.parse(parseInt(time, 10) * 1000) : dateTime.parse(time);
894922
if (!dateTime.isValid(newDate)) {
@@ -915,18 +943,13 @@ class BoxCommand extends Command {
915943
newDate = new Date();
916944
} else {
917945

918-
this.error(`Cannot parse date format "${time}"`);
946+
throw new BoxCLIError(`Cannot parse date format "${time}"`);
919947
}
920948
}
921949

922-
// Dates are always in the user's local timezone, but for
923-
// consistency we move them all to UTC
924-
925-
926950
// Format the timezone to RFC3339 format for the Box API
927-
// Also always use UTC for consistency
928-
return newDate.toISOString().replace(/\.\d{3}Z$/u, '-00:00');
929-
// return dateTime.format(newDate, 'YYYY-MM-DDTHH:mm:ssZ');
951+
// Also always use UTC timezone for consistency in tests
952+
return newDate.toISOString().replace(/\.\d{3}Z$/u, '+00:00');
930953
}
931954

932955
/**

src/commands/collaborations/create.js

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,15 @@
22

33
const BoxCommand = require('../../box-command');
44
const { flags } = require('@oclif/command');
5+
const CollaborationModule = require('../../modules/collaboration');
56

67
class CollaborationsAddCommand extends BoxCommand {
78

89
async run() {
910
const { flags, args } = this.parse(CollaborationsAddCommand);
10-
let params = {
11-
body: {
12-
item: {
13-
type: args.itemType,
14-
id: args.itemID,
15-
},
16-
accessible_by: {}
17-
},
18-
qs: {}
19-
};
2011

21-
if (flags.fields) {
22-
params.qs.fields = flags.fields;
23-
}
24-
if (flags.hasOwnProperty('notify')) {
25-
params.qs.notify = flags.notify;
26-
}
27-
if (flags.hasOwnProperty('can-view-path')) {
28-
params.body.can_view_path = flags['can-view-path'];
29-
}
30-
if (flags.role) {
31-
params.body.role = flags.role.replace('_', ' ');
32-
} else if (flags.editor) {
33-
params.body.role = this.client.collaborationRoles.EDITOR;
34-
} else if (flags.viewer) {
35-
params.body.role = this.client.collaborationRoles.VIEWER;
36-
} else if (flags.previewer) {
37-
params.body.role = this.client.collaborationRoles.PREVIEWER;
38-
} else if (flags.uploader) {
39-
params.body.role = this.client.collaborationRoles.UPLOADER;
40-
} else if (flags['previewer-uploader']) {
41-
params.body.role = this.client.collaborationRoles.PREVIEWER_UPLOADER;
42-
} else if (flags['viewer-uploader']) {
43-
params.body.role = this.client.collaborationRoles.VIEWER_UPLOADER;
44-
} else if (flags['co-owner']) {
45-
params.body.role = this.client.collaborationRoles.CO_OWNER;
46-
}
47-
48-
if (flags['user-id']) {
49-
params.body.accessible_by.type = 'user';
50-
params.body.accessible_by.id = flags['user-id'];
51-
} else if (flags['group-id']) {
52-
params.body.accessible_by.type = 'group';
53-
params.body.accessible_by.id = flags['group-id'];
54-
} else if (flags.login) {
55-
params.body.accessible_by.type = 'user';
56-
params.body.accessible_by.login = flags.login;
57-
}
58-
59-
// @TODO (2018-07-07): Should implement this using the Node SDK
60-
let collaboration = await this.client.wrapWithDefaultHandler(this.client.post)('/collaborations', params);
12+
let collabModule = new CollaborationModule(this.client);
13+
let collaboration = await collabModule.createCollaboration(args, flags);
6114
await this.output(collaboration);
6215
}
6316
}

src/commands/collaborations/update.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class CollaborationsUpdateCommand extends BoxCommand {
3737
params.body.role = this.client.collaborationRoles.OWNER;
3838
}
3939
if (flags['expires-at']) {
40-
params.body.expires_at = this.getDateFromString(flags['expires-at']);
40+
params.body.expires_at = flags['expires-at'];
4141
}
4242

4343
// @TODO (2018-07-07): Should implement this using the Node SDK
@@ -205,6 +205,7 @@ CollaborationsUpdateCommand.flags = {
205205
}),
206206
'expires-at': flags.string({
207207
description: 'When the collaboration should expire',
208+
parse: input => BoxCommand.normalizeDateString(input),
208209
})
209210
};
210211

src/commands/events/index.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ class EventsGetCommand extends BoxCommand {
2525
if (flags['stream-position']) {
2626
options.stream_position = flags['stream-position'];
2727
} else if (options.stream_type === 'admin_logs') {
28-
options.created_before = this.getDateFromString(flags['created-before'] || DEFAULT_END_TIME);
28+
options.created_before = flags['created-before'] || BoxCommand.normalizeDateString(DEFAULT_END_TIME);
2929

3030
if (flags['created-before'] && !flags['created-after']) {
3131
// If the user specified an end time but no start time, recompute the default start time as
3232
// the specified end time minus five days
3333
let endTime = date.addDays(date.parse(options.created_before), -5);
34-
options.created_after = endTime.toISOString().replace(/\.\d{3}Z$/u, '-00:00');
34+
options.created_after = endTime.toISOString().replace(/\.\d{3}Z$/u, '+00:00');
3535
} else {
36-
options.created_after = this.getDateFromString(flags['created-after'] || DEFAULT_START_TIME);
36+
options.created_after = flags['created-after'] || BoxCommand.normalizeDateString(DEFAULT_START_TIME);
3737
}
3838
}
3939

@@ -70,14 +70,16 @@ EventsGetCommand.flags = {
7070
description: 'Get enterprise events'
7171
}),
7272
'created-after': flags.string({
73-
description: 'Return enterprise events that occured after a time. Use a timestamp or shorthand syntax 0t, like 5w for 5 weeks. If not used, defaults to 5 days before the end date',
73+
description: 'Return enterprise events that occurred after a time. Use a timestamp or shorthand syntax 0t, like 5w for 5 weeks. If not used, defaults to 5 days before the end date',
7474
exclusive: ['stream_position'],
7575
dependsOn: ['enterprise'],
76+
parse: input => BoxCommand.normalizeDateString(input),
7677
}),
7778
'created-before': flags.string({
78-
description: 'Return enterprise events that occured before a time. Use a timestamp or shorthand syntax 0t, like 5w for 5 weeks. If not used, defaults to now',
79+
description: 'Return enterprise events that occurred before a time. Use a timestamp or shorthand syntax 0t, like 5w for 5 weeks. If not used, defaults to now',
7980
exclusive: ['stream_position'],
8081
dependsOn: ['enterprise'],
82+
parse: input => BoxCommand.normalizeDateString(input),
8183
}),
8284
'event-types': flags.string({
8385
description: 'Return enterprise events filtered by event types. Format using a comma delimited list: NEW_USER,DELETE_USER,EDIT_USER',

src/commands/events/poll.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ class EventsPollCommand extends BoxCommand {
1414
options.event_type = flags['event-types'];
1515
}
1616
if (flags['start-date']) {
17-
options.startDate = this.getDateFromString(flags['start-date']);
17+
options.startDate = flags['start-date'];
1818
}
1919
if (flags['end-date']) {
20-
options.endDate = this.getDateFromString(flags['end-date']);
20+
options.endDate = flags['end-date'];
2121
}
2222
if (flags['polling-interval']) {
2323
options.pollingInterval = flags['polling-interval'];
@@ -48,8 +48,14 @@ EventsPollCommand.flags = {
4848
description: 'Get enterprise events'
4949
}),
5050
'event-types': flags.string({ description: 'Return enterprise events filtered by event types. Format using a comma delimited list: NEW_USER,DELETE_USER,EDIT_USER' }),
51-
'start-date': flags.string({ description: 'Return enterprise events that occured after this time. Use a timestamp or shorthand syntax 00t, like 05w for 5 weeks. If not used, defaults to now' }),
52-
'end-date': flags.string({ description: 'Return enterprise events that occured before this time. Use a timestamp or shorthand syntax 00t, like 05w for 5 weeks.' }),
51+
'start-date': flags.string({
52+
description: 'Return enterprise events that occured after this time. Use a timestamp or shorthand syntax 00t, like 05w for 5 weeks. If not used, defaults to now',
53+
parse: input => BoxCommand.normalizeDateString(input),
54+
}),
55+
'end-date': flags.string({
56+
description: 'Return enterprise events that occured before this time. Use a timestamp or shorthand syntax 00t, like 05w for 5 weeks.',
57+
parse: input => BoxCommand.normalizeDateString(input),
58+
}),
5359
'polling-interval': flags.string({ description: 'Number of seconds to wait before polling for new events. Default is 60 seconds.' })
5460
};
5561

src/commands/files/collaborations/add.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33
const BoxCommand = require('../../../box-command');
44
const { flags } = require('@oclif/command');
55
const CollaborationsCreateCommand = require('../../collaborations/create');
6+
const CollaborationModule = require('../../../modules/collaboration');
67

78
class FilesCollaborationsAddCommand extends BoxCommand {
8-
run() {
9-
const { args } = this.parse(FilesCollaborationsAddCommand);
9+
async run() {
10+
const { args, flags } = this.parse(FilesCollaborationsAddCommand);
1011

11-
// Clone the args and insert the "file" type at the correct position for the generic command
12-
let argv = this.argv.slice();
13-
argv.splice(argv.indexOf(args.id) + 1, 0, 'file');
14-
return CollaborationsCreateCommand.run(argv);
12+
// Transform arguments for generic module
13+
args.itemType = 'file';
14+
args.itemID = args.id;
15+
delete args.id;
16+
17+
let collabModule = new CollaborationModule(this.client);
18+
let collaboration = await collabModule.createCollaboration(args, flags);
19+
await this.output(collaboration);
1520
}
1621
}
1722

src/commands/files/lock.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class FilesLockCommand extends BoxCommand {
99
let options = {};
1010

1111
if (flags.expires) {
12-
options.expires_at = this.getDateFromString(flags.expires);
12+
options.expires_at = flags.expires;
1313
}
1414
if (flags.hasOwnProperty('prevent-download')) {
1515
options.is_download_prevented = flags['prevent-download'];
@@ -26,7 +26,10 @@ FilesLockCommand.description = 'Lock a file';
2626

2727
FilesLockCommand.flags = {
2828
...BoxCommand.flags,
29-
expires: flags.string({ description: 'Make the lock expire from a timespan set from now. Use s for seconds, m for minutes, h for hours, d for days, w for weeks, M for months. For example, 30 seconds is 30s' }),
29+
expires: flags.string({
30+
description: 'Make the lock expire from a timespan set from now. Use s for seconds, m for minutes, h for hours, d for days, w for weeks, M for months. For example, 30 seconds is 30s',
31+
parse: input => BoxCommand.normalizeDateString(input),
32+
}),
3033
'prevent-download': flags.boolean({
3134
description: 'Prevent download of locked file',
3235
allowNo: true

src/commands/files/share.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33
const BoxCommand = require('../../box-command');
44
const { flags } = require('@oclif/command');
55
const SharedLinksCreateCommand = require('../shared-links/create');
6+
const SharedLinksModule = require('../../modules/shared-links');
67

78
class FilesShareCommand extends BoxCommand {
8-
run() {
9-
const { args } = this.parse(FilesShareCommand);
9+
async run() {
10+
const { args, flags } = this.parse(FilesShareCommand);
1011

11-
// Clone the args and insert the "file" type at the correct position for the generic command
12-
let argv = this.argv.slice();
13-
argv.splice(argv.indexOf(args.id) + 1, 0, 'file');
14-
return SharedLinksCreateCommand.run(argv);
12+
// Transform arguments for generic module
13+
args.itemType = 'file';
14+
args.itemID = args.id;
15+
delete args.id;
16+
17+
let sharedLinksModule = new SharedLinksModule(this.client);
18+
let updatedItem = await sharedLinksModule.createSharedLink(args, flags);
19+
await this.output(updatedItem.shared_link);
1520
}
1621
}
1722

src/commands/files/unshare.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22

33
const BoxCommand = require('../../box-command');
44
const SharedLinksDeleteCommand = require('../shared-links/delete');
5+
const SharedLinksModule = require('../../modules/shared-links');
6+
const chalk = require('chalk');
57

68
class FilesUnshareCommand extends BoxCommand {
7-
run() {
9+
async run() {
810
const { args } = this.parse(FilesUnshareCommand);
911

10-
// Clone the args and insert the "file" type at the correct position for the generic command
11-
let argv = this.argv.slice();
12-
argv.splice(argv.indexOf(args.id) + 1, 0, 'file');
13-
return SharedLinksDeleteCommand.run(argv);
12+
// Transform arguments for generic module
13+
args.itemType = 'file';
14+
args.itemID = args.id;
15+
delete args.id;
16+
17+
let sharedLinksModule = new SharedLinksModule(this.client);
18+
let item = await sharedLinksModule.removeSharedLink(args);
19+
this.info(chalk`{green Removed shared link from ${args.itemType} "${item.name}"}`);
1420
}
1521
}
1622

0 commit comments

Comments
 (0)