box-node-sdk 1.26.1 → 1.29.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.29.0 [2019-04-25]
4
+
5
+ - Added convenience methods for setting metadata on [files](./docs/metadata.md#set-metadata-on-a-file)
6
+ and [folders](./docs/metadata.md#set-metadata-on-a-folder) ([#376](https://github.com/box/box-node-sdk/pull/376))
7
+
8
+ ## 1.28.0 [2019-03-28]
9
+
10
+ - Added methods for [moving](./docs/web-links.md#move-a-web-link) and [copying](./docs/web-links.md#move-a-web-link)
11
+ weblinks, as well as [adding or removing from a collection](./docs/web-links.md#add-web-link-to-a-collection)
12
+
13
+ ## 1.27.0 [2019-02-28]
14
+
15
+ - Added the trace ID from API response headers to error messages for easier debugging
16
+ - Added more safety checks in the error flow to protect against throwing when handling a malformed request
17
+ - Added support for [retrieving a user's avatar image](./docs/users.md#get-user-avatar)
18
+
19
+ ## 1.26.2 [2019-02-22]
20
+
21
+ - Fixed an error where under high request rates, code in the error handling logic could throw when handling a
22
+ malformed request
23
+
3
24
  ## 1.26.1 [2019-02-12]
4
25
 
5
26
  - Fixed an error where some methods could throw an error when constructing an iterator
@@ -339,7 +339,12 @@ APIRequest.prototype._retry = function(err) {
339
339
  APIRequest.prototype._finish = function(err, response) {
340
340
  var callback = this._callback;
341
341
  process.nextTick(() => {
342
- callback(err, response);
342
+ if (err) {
343
+ callback(err);
344
+ return;
345
+ }
346
+
347
+ callback(null, response);
343
348
  });
344
349
  };
345
350
 
@@ -743,6 +743,37 @@ Files.prototype.updateMetadata = function(fileID, scope, template, patch, callba
743
743
  return this.client.wrapWithDefaultHandler(this.client.put)(apiPath, params, callback);
744
744
  };
745
745
 
746
+ /**
747
+ * Sets metadata on a file, overwriting any metadata that exists for the provided keys.
748
+ *
749
+ * @param {string} fileID - The file to set metadata on
750
+ * @param {string} scope - The scope of the metadata template
751
+ * @param {string} template - The key of the metadata template
752
+ * @param {Object} metadata - The metadata to set
753
+ * @param {Function} [callback] - Called with updated metadata if successful
754
+ * @returns {Promise<Object>} A promise resolving to the updated metadata
755
+ */
756
+ Files.prototype.setMetadata = function(fileID, scope, template, metadata, callback) {
757
+
758
+ return this.addMetadata(fileID, scope, template, metadata)
759
+ .catch(err => {
760
+
761
+ if (err.statusCode !== 409) {
762
+ throw err;
763
+ }
764
+
765
+ // Metadata already exists on the file; update instead
766
+ var updates = Object.keys(metadata).map(key => ({
767
+ op: 'add',
768
+ path: `/${key}`,
769
+ value: metadata[key],
770
+ }));
771
+
772
+ return this.updateMetadata(fileID, scope, template, updates);
773
+ })
774
+ .asCallback(callback);
775
+ };
776
+
746
777
  /**
747
778
  * Deletes a metadata template from a file.
748
779
  *
@@ -368,6 +368,37 @@ Folders.prototype.updateMetadata = function(folderID, scope, template, patch, ca
368
368
  return this.client.wrapWithDefaultHandler(this.client.put)(apiPath, params, callback);
369
369
  };
370
370
 
371
+ /**
372
+ * Sets metadata on a folder, overwriting any metadata that exists for the provided keys.
373
+ *
374
+ * @param {string} folderID - The folder to set metadata on
375
+ * @param {string} scope - The scope of the metadata template
376
+ * @param {string} template - The key of the metadata template
377
+ * @param {Object} metadata - The metadata to set
378
+ * @param {Function} [callback] - Called with updated metadata if successful
379
+ * @returns {Promise<Object>} A promise resolving to the updated metadata
380
+ */
381
+ Folders.prototype.setMetadata = function(folderID, scope, template, metadata, callback) {
382
+
383
+ return this.addMetadata(folderID, scope, template, metadata)
384
+ .catch(err => {
385
+
386
+ if (err.statusCode !== 409) {
387
+ throw err;
388
+ }
389
+
390
+ // Metadata already exists on the file; update instead
391
+ var updates = Object.keys(metadata).map(key => ({
392
+ op: 'add',
393
+ path: `/${key}`,
394
+ value: metadata[key],
395
+ }));
396
+
397
+ return this.updateMetadata(folderID, scope, template, updates);
398
+ })
399
+ .asCallback(callback);
400
+ };
401
+
371
402
  /**
372
403
  * Deletes a metadata template from a folder.
373
404
  *
@@ -78,6 +78,8 @@ Search.prototype = {
78
78
  * @param {SearchMetadataFilter[]} [options.mdfilters] - Searches for objects with a specific metadata object association. Searches with the this parameter do not require a query string
79
79
  * @param {int} [options.limit=30] - The number of search results to return, max 200
80
80
  * @param {int} [options.offset=0] - The search result at which to start the response, must be a multiple of limit
81
+ * @param {string} [options.sort] - The field on which the results should be sorted, e.g. "modified_at"
82
+ * @param {string} [options.direction] - The sort direction: "ASC" for ascending and "DESC" for descending
81
83
  * @param {APIRequest~Callback} [callback] - passed the new comment data if it was posted successfully
82
84
  * @returns {Promise<Object>} A promise resolving to the collection of search results
83
85
  */
@@ -189,6 +189,27 @@ Users.prototype.getGroupMemberships = function(userID, options, callback) {
189
189
  return this.client.wrapWithDefaultHandler(this.client.get)(apiPath, params, callback);
190
190
  };
191
191
 
192
+ /**
193
+ * Retrieve the user's avatar image.
194
+ *
195
+ * API Endpoint: '/users/:userID/avatar'
196
+ * Method: GET
197
+ *
198
+ * @param {string} userID The ID of the user whose avatar should be retrieved
199
+ * @param {Function} [callback] Passed a stream over the bytes of the avatar image if successful
200
+ * @returns {Promise<Readable>} A promise resolving to the image stream
201
+ */
202
+ Users.prototype.getAvatar = function(userID, callback) {
203
+
204
+ var apiPath = urlPath(BASE_PATH, userID, 'avatar'),
205
+ params = {
206
+ streaming: true
207
+ };
208
+
209
+ return this.client.get(apiPath, params)
210
+ .asCallback(callback);
211
+ };
212
+
192
213
  // @NOTE(fschott) 2014-05-06: Still need to implement get, edit, create, etc.
193
214
  // The problem is that they are only available to enterprise admins, so we'll
194
215
  // first need to figure out how we want to handle access to those methods.
@@ -89,7 +89,7 @@ WebLinks.prototype.get = function(weblinkID, options, callback) {
89
89
  * @param {Object} updates - Fields of the weblink to update
90
90
  * @param {string} [updates.name] - Name for the web link. Will default to the URL if empty.
91
91
  * @param {string} [updates.description] - Description of the web link. Will provide more context to users about the web link.
92
- * @param {Function} [callback] - Passed the updated web-link information if it was acquired successfully, error otherwise
92
+ * @param {Function} [callback] - Passed the updated web link information if it was acquired successfully, error otherwise
93
93
  * @returns {Promise<Object>} A promise resolving to the updated web link object
94
94
  */
95
95
  WebLinks.prototype.update = function(weblinkID, updates, callback) {
@@ -117,4 +117,111 @@ WebLinks.prototype.delete = function(weblinkID, callback) {
117
117
  return this.client.wrapWithDefaultHandler(this.client.del)(apiPath, null, callback);
118
118
  };
119
119
 
120
+ /**
121
+ * Move a web link into a new parent folder.
122
+ *
123
+ * API Endpoint: '/web_links/:webLinkID'
124
+ * Method: PUT
125
+ *
126
+ * @param {string} webLinkID - The Box ID of the web link being requested
127
+ * @param {string} newParentID - The Box ID for the new parent folder. '0' to move to All Files.
128
+ * @param {Function} [callback] - Passed the updated web link information if it was acquired successfully
129
+ * @returns {Promise<Object>} A promise resolving to the updated web link object
130
+ */
131
+ WebLinks.prototype.move = function(webLinkID, newParentID, callback) {
132
+ var params = {
133
+ body: {
134
+ parent: {
135
+ id: newParentID
136
+ }
137
+ }
138
+ };
139
+ var apiPath = urlPath(BASE_PATH, webLinkID);
140
+ return this.client.wrapWithDefaultHandler(this.client.put)(apiPath, params, callback);
141
+ };
142
+
143
+ /**
144
+ * Copy a web link into a new, different folder
145
+ *
146
+ * API Endpoint: '/web_links/:webLinkID/copy
147
+ * Method: POST
148
+ *
149
+ * @param {string} webLinkID - The Box ID of the web link being requested
150
+ * @param {string} newParentID - The Box ID for the new parent folder. '0' to copy to All Files.
151
+ * @param {Object} [options] - Optional parameters for the copy operation, can be left null in most cases
152
+ * @param {string} [options.name] - A new name to use if there is an identically-named item in the new parent folder
153
+ * @param {Function} [callback] - passed the new web link info if call was successful
154
+ * @returns {Promise<Object>} A promise resolving to the new web link object
155
+ */
156
+ WebLinks.prototype.copy = function(webLinkID, newParentID, options, callback) {
157
+
158
+ options = options || {};
159
+
160
+ options.parent = {
161
+ id: newParentID
162
+ };
163
+
164
+ var params = {
165
+ body: options
166
+ };
167
+ var apiPath = urlPath(BASE_PATH, webLinkID, '/copy');
168
+ return this.client.wrapWithDefaultHandler(this.client.post)(apiPath, params, callback);
169
+ };
170
+
171
+ /**
172
+ * Add a web link to a given collection
173
+ *
174
+ * API Endpoint: '/web_links/:webLinkID'
175
+ * Method: PUT
176
+ *
177
+ * @param {string} webLinkID - The web link to add to the collection
178
+ * @param {string} collectionID - The collection to add the web link to
179
+ * @param {Function} [callback] - Passed the updated web link if successful, error otherwise
180
+ * @returns {Promise<Object>} A promise resolving to the updated web link object
181
+ */
182
+ WebLinks.prototype.addToCollection = function(webLinkID, collectionID, callback) {
183
+
184
+ return this.get(webLinkID, {fields: 'collections'})
185
+ .then(data => {
186
+
187
+ var collections = data.collections || [];
188
+
189
+ // Convert to correct format
190
+ collections = collections.map(c => ({id: c.id}));
191
+
192
+ if (!collections.find(c => c.id === collectionID)) {
193
+
194
+ collections.push({id: collectionID});
195
+ }
196
+
197
+ return this.update(webLinkID, {collections});
198
+ })
199
+ .asCallback(callback);
200
+ };
201
+
202
+ /**
203
+ * Remove a web link from a given collection
204
+ *
205
+ * API Endpoint: '/web_links/:webLinkID'
206
+ * Method: PUT
207
+ *
208
+ * @param {string} webLinkID - The web link to remove from the collection
209
+ * @param {string} collectionID - The collection to remove the web link from
210
+ * @param {Function} [callback] - Passed the updated web link if successful, error otherwise
211
+ * @returns {Promise<Object>} A promise resolving to the updated web link object
212
+ */
213
+ WebLinks.prototype.removeFromCollection = function(webLinkID, collectionID, callback) {
214
+
215
+ return this.get(webLinkID, {fields: 'collections'})
216
+ .then(data => {
217
+
218
+ var collections = data.collections || [];
219
+ // Convert to correct object format and remove the specified collection
220
+ collections = collections.map(c => ({id: c.id})).filter(c => c.id !== collectionID);
221
+
222
+ return this.update(webLinkID, {collections});
223
+ })
224
+ .asCallback(callback);
225
+ };
226
+
120
227
  module.exports = WebLinks;
@@ -10,6 +10,7 @@
10
10
  var qs = require('querystring'),
11
11
  httpStatusCodes = require('http-status');
12
12
 
13
+ const TRACE_ID_HEADER_NAME = 'box-request-id';
13
14
 
14
15
  // ------------------------------------------------------------------------------
15
16
  // Typedefs and Callbacks
@@ -37,14 +38,18 @@ var qs = require('querystring'),
37
38
  */
38
39
  function Request(req) {
39
40
  this.method = req.method;
40
- this.url = {
41
- protocol: req.uri.protocol,
42
- host: req.uri.host,
43
- path: req.uri.pathname,
44
- query: qs.parse(req.uri.query),
45
- fragment: req.uri.hash
46
- };
47
- this.httpVersion = req.response.httpVersion;
41
+ if (req.uri) {
42
+ this.url = {
43
+ protocol: req.uri.protocol,
44
+ host: req.uri.host,
45
+ path: req.uri.pathname,
46
+ query: qs.parse(req.uri.query),
47
+ fragment: req.uri.hash
48
+ };
49
+ } else {
50
+ this.url = null;
51
+ }
52
+ this.httpVersion = req.response ? req.response.httpVersion : null;
48
53
  this.headers = req.headers;
49
54
  this.body = req.body;
50
55
  }
@@ -76,23 +81,42 @@ module.exports = {
76
81
  message = message || 'API Response Error';
77
82
 
78
83
  var statusCode = response.statusCode;
79
- var requestID = '';
80
- if (response.body && response.body.request_id) {
81
- requestID = ` | ${response.body.request_id}`;
84
+ var statusMessage = httpStatusCodes[statusCode];
85
+ var debugID = ''; // Of the form <requestID>.<traceID>, both parts optional
86
+ var errorCode;
87
+ var errorDescription;
88
+
89
+ if (response.headers && response.headers[TRACE_ID_HEADER_NAME]) {
90
+ // Append trace ID with dot separator — if not present, the dot should be omitted
91
+ debugID += `.${response.headers[TRACE_ID_HEADER_NAME]}`;
82
92
  }
83
93
 
84
- var apiMessage = '';
85
94
 
86
95
  if (response.body) {
87
96
 
88
- var code = response.body.code || response.body.error;
89
- var description = response.body.message || response.body.error_description;
97
+ if (response.body.request_id) {
98
+ // Prepend request ID
99
+ debugID = response.body.request_id + debugID;
100
+ }
101
+
102
+ errorCode = response.body.code || response.body.error;
103
+ errorDescription = response.body.message || response.body.error_description;
104
+ }
105
+
106
+ var errorMessage;
107
+ if (debugID) {
108
+ errorMessage = `${message} [${statusCode} ${statusMessage} | ${debugID}]`;
109
+ } else {
110
+ errorMessage = `${message} [${statusCode} ${statusMessage}]`;
111
+ }
90
112
 
91
- apiMessage += code ? ` ${code}` : '';
92
- apiMessage += description ? ` - ${description}` : '';
113
+ if (errorCode) {
114
+ errorMessage += ` ${errorCode}`;
115
+ }
116
+ if (errorDescription) {
117
+ errorMessage += ` - ${errorDescription}`;
93
118
  }
94
119
 
95
- var errorMessage = `${message} [${statusCode} ${httpStatusCodes[statusCode]}${requestID}]${apiMessage}`;
96
120
  var responseError = new Error(errorMessage);
97
121
 
98
122
  responseError.statusCode = response.statusCode;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "box-node-sdk",
3
3
  "author": "Box <oss@box.com>",
4
- "version": "1.26.1",
4
+ "version": "1.29.0",
5
5
  "description": "Official SDK for Box Plaform APIs",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -58,7 +58,7 @@
58
58
  "nyc": "^11.4.1",
59
59
  "shelljs": "^0.8.1",
60
60
  "shelljs-nodecli": "^0.1.1",
61
- "sinon": "^6.0.0"
61
+ "sinon": "^7.2.4"
62
62
  },
63
63
  "files": [
64
64
  "config",