particle-api-js 10.3.0 → 10.3.1

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/src/Agent.js CHANGED
@@ -23,385 +23,409 @@ const qs = require('qs');
23
23
  const fs = require('../fs');
24
24
  const packageJson = require('../package.json');
25
25
 
26
+ /**
27
+ * @typedef {string} AccessToken
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} BasicAuth
32
+ * @property {string} username
33
+ * @property {string} password
34
+ */
35
+
36
+ /**
37
+ * @typedef {AccessToken | BasicAuth} Auth Prefer using an access token over basic auth for better security
38
+ */
39
+
26
40
  /**
27
41
  * The object returned for a basic request
28
42
  * @typedef {object} JSONResponse
29
- * @property {number} statusCode The HTTP response status
30
- * @property {object} body The endpoint's response parsed as a JSON
43
+ * @property {number} statusCode The HTTP response status
44
+ * @property {object} body The endpoint's response parsed as a JSON
31
45
  */
32
46
 
33
47
  /**
34
48
  * The possible response from an API request
35
- * @typedef {JSONResponse|Buffer|ArrayBuffer} RequestResponse The type is based on
49
+ * @typedef {JSONResponse | Buffer | ArrayBuffer} RequestResponse The type is based on
36
50
  * the request config and whether is on browser or node
37
51
  */
38
52
 
39
53
  /**
40
54
  * The error object generated in case of a failed request
41
55
  * @typedef {object} RequestError
42
- * @property {number} statusCode The HTTP response status
43
- * @property {string} errorDescription Details on what caused the failed request
44
- * @property {string} shortErrorDescription Summarized version of the fail reason
45
- * @property {object} body The response object from the request
46
- * @property {object} error The error object from the request
56
+ * @property {number} statusCode The HTTP response status
57
+ * @property {string} errorDescription Details on what caused the failed request
58
+ * @property {string} shortErrorDescription Summarized version of the fail reason
59
+ * @property {object} body The response object from the request
60
+ * @property {object} error The error object from the request
47
61
  */
48
62
 
49
63
  class Agent {
50
- constructor(baseUrl){
51
- this.setBaseUrl(baseUrl);
52
- }
53
-
54
- setBaseUrl(baseUrl) {
55
- this.baseUrl = baseUrl;
56
- }
57
-
58
- /**
59
- * Make a GET request
60
- * @param {object} params Configurations to customize the request
61
- * @param {string} params.uri The URI to request
62
- * @param {string|object} [params.auth] Authorization token to use
63
- * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
64
- * @param {string|object} [params.query] Key/Value pairs of query params or a correctly formatted string
65
- * @param {object} [params.context] The invocation context, describing the tool and project
66
- * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
67
- */
68
- get({ uri, auth, headers, query, context }) {
69
- return this.request({ uri, method: 'get', auth, headers, query, context });
70
- }
71
-
72
- /**
73
- * Make a HEAD request
74
- * @param {object} params Configurations to customize the request
75
- * @param {string} params.uri The URI to request
76
- * @param {string|object} [params.auth] Authorization token to use
77
- * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
78
- * @param {string|object} [params.query] Key/Value pairs of query params or a correctly formatted string
79
- * @param {object} [params.context] The invocation context, describing the tool and project
80
- * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
81
- */
82
- head({ uri, auth, headers, query, context }) {
83
- return this.request({ uri, method: 'head', auth, headers, query, context });
84
- }
85
-
86
- /**
87
- * Make a POST request
88
- * @param {object} params Configurations to customize the request
89
- * @param {string} params.uri The URI to request
90
- * @param {string|object} [params.auth] Authorization token to use
91
- * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
92
- * @param {object} [params.data] Key/Value pairs of query params or a correctly formatted string
93
- * @param {object} [params.context] The invocation context, describing the tool and project
94
- * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
95
- */
96
- post({ uri, headers, data, auth, context }) {
97
- return this.request({ uri, method: 'post', auth, headers, data, context });
98
- }
99
-
100
- /**
101
- * Make a PUT request
102
- * @param {object} params Configurations to customize the request
103
- * @param {string} params.uri The URI to request
104
- * @param {string|object} [params.auth] Authorization token to use
105
- * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
106
- * @param {object} [params.data] Key/VAlue pairs of query params or a correctly formatted string
107
- * @param {object} [params.context] The invocation context, describing the tool and project
108
- * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
109
- */
110
- put({ uri, auth, headers, data, context }) {
111
- return this.request({ uri, method: 'put', auth, headers, data, context });
112
- }
113
-
114
- /**
115
- * Make a DELETE request
116
- * @param {object} params Configurations to customize the request
117
- * @param {string} params.uri The URI to request
118
- * @param {string|object} [params.auth] Authorization token to use
119
- * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
120
- * @param {object} [params.data] Key/Value pairs of query params or a correctly formatted string
121
- * @param {object} [params.context] The invocation context, describing the tool and project
122
- * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
123
- */
124
- delete({ uri, auth, headers, data, context }) {
125
- return this.request({ uri, method: 'delete', auth, headers, data, context });
126
- }
127
-
128
- /**
129
- *
130
- * @param {object} config An obj with all the possible request configurations
131
- * @param {string} config.uri The URI to request
132
- * @param {string} config.method The method used to request the URI, should be in uppercase.
133
- * @param {object} [config.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
134
- * @param {object} [config.data] Arbitrary data to send as the body.
135
- * @param {string|object} [config.auth] Authorization
136
- * @param {string|object} [config.query] Query parameters
137
- * @param {object} [config.form] Form fields
138
- * @param {object} [config.files] Array of file names and file content
139
- * @param {object} [config.context] The invocation context, describing the tool and project.
140
- * @param {boolean} [config.isBuffer=false] Indicate if the response should be treated as Buffer instead of JSON
141
- * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
142
- */
143
- request({
144
- uri,
145
- method,
146
- headers = undefined,
147
- data = undefined,
148
- auth,
149
- query = undefined,
150
- form = undefined,
151
- files = undefined,
152
- context = undefined,
153
- isBuffer = false
154
- }){
155
- const requestFiles = this._sanitizeFiles(files);
156
- const requestParams = this._buildRequest({ uri, method, headers, data, auth, query, form, context, files: requestFiles });
157
- return this._promiseResponse(requestParams, isBuffer);
158
- }
159
-
160
- /**
161
- * Promises to send the request and retreive the response.
162
- * @param {[string, object]} requestParams First argument is the URI to request, the second one are the options.
163
- * @param {boolean} isBuffer Indicate if the response body should be returned as a Buffer (Node) / ArrayBuffer (browser) instead of JSON
164
- * @param {function} [makerequest=fetch] The fetch function to use. Override for testing.
165
- * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
166
- * @private
167
- */
168
- _promiseResponse(requestParams, isBuffer, makerequest = fetch) {
169
- let status;
170
- return makerequest(...requestParams)
171
- .then((resp) => {
172
- status = resp.status;
173
- if (!resp.ok) {
174
- return resp.text().then((err) => {
175
- const objError = JSON.parse(err);
176
- // particle-commnds/src/cmd/api expects response.text. to be a string
177
- const response = Object.assign(resp, { text: err });
178
- throw Object.assign(objError, { response });
179
- });
180
- }
181
- if (status === 204) { // Can't do resp.json() since there is no body to parse
182
- return '';
183
- }
184
- if (isBuffer) {
185
- return resp.blob();
186
- }
187
- return resp.json();
188
- }).then((body) => {
189
- if (isBuffer) {
190
- return body.arrayBuffer().then((arrayBuffer) => {
191
- if (!this.isForBrowser()) {
192
- return Buffer.from(arrayBuffer);
193
- }
194
- return arrayBuffer;
195
- });
196
- }
197
- return {
198
- body,
199
- statusCode: status
200
- };
201
- }).catch((error) => {
202
- const errorType = status ? `HTTP error ${status}` : 'Network error';
203
- let errorDescription = `${errorType} from ${requestParams[0]}`;
204
- let shortErrorDescription;
205
- if (error.error_description) { // Fetch responded with ok false
206
- errorDescription = `${errorDescription} - ${error.error_description}`;
207
- shortErrorDescription = error.error_description;
208
- }
209
- const reason = new Error(errorDescription);
210
- Object.assign(reason, {
211
- statusCode: status,
212
- errorDescription,
213
- shortErrorDescription,
214
- error,
215
- body: error
216
- });
217
- throw reason;
218
- });
219
- }
220
-
221
- /**
222
- * Generate the params in a format valid for 'fetch'
223
- * @returns {[string, object]} The uri to make the request too, and extra configs
224
- * @private
225
- */
226
- _buildRequest({ uri, method, headers, data, auth, query, form, files, context }){
227
- let actualUri = uri;
228
- if (this.baseUrl && uri[0] === '/') {
229
- actualUri = `${this.baseUrl}${uri}`;
230
- }
231
- if (query) {
232
- const queryParams = qs.stringify(query);
233
- const hasParams = actualUri.includes('?');
234
- actualUri = `${actualUri}${hasParams ? '&' : '?'}${queryParams}`;
235
- }
236
-
237
- const userAgentHeader = { 'User-Agent': `${packageJson.name}/${packageJson.version} (${packageJson.repository.url})` };
238
- let body;
239
- let contentTypeHeader;
240
- if (files){
241
- // @ts-ignore
242
- contentTypeHeader = {}; // Needed to allow fetch create its own
243
- body = this._getFromData(files, form);
244
- } else if (form){
245
- contentTypeHeader = { 'Content-Type': 'application/x-www-form-urlencoded' };
246
- body = qs.stringify(form);
247
- } else if (data){
248
- contentTypeHeader = { 'Content-Type': 'application/json' };
249
- body = JSON.stringify(data);
250
- }
251
- const finalHeaders = Object.assign({},
252
- userAgentHeader,
253
- contentTypeHeader,
254
- this._getAuthorizationHeader(auth),
255
- this._getContextHeaders(context),
256
- headers
257
- );
258
-
259
- return [actualUri, { method, body, headers: finalHeaders }];
260
- }
261
-
262
- isForBrowser() {
263
- return typeof window !== 'undefined';
264
- }
265
-
266
- _getFromData(files, form) {
267
- const formData = new FormData();
268
- for (let [name, file] of Object.entries(files)){
269
- let path = file.path;
270
- let fileData = file.data;
271
- if (!this.isForBrowser()) {
272
- const nodeFormData = this._getNodeFormData(file);
273
- path = nodeFormData.path;
274
- fileData = nodeFormData.file;
275
- }
276
- formData.append(name, fileData, path);
277
- }
278
- if (form){
279
- for (let [name, value] of Object.entries(form)){
280
- formData.append(name, value);
281
- }
282
- }
283
- return formData;
284
- }
285
-
286
- _getNodeFormData(file) {
287
- let fileData = file.data;
288
- if (typeof file.data === 'string') {
289
- fileData = fs.createReadStream(file.data);
290
- }
291
- return {
292
- file: fileData,
293
- path: { filepath: file.path } // Different API for nodejs
294
- };
295
- }
296
-
297
- _getContextHeaders(context = {}) {
298
- return Object.assign({},
299
- this._getToolContext(context.tool),
300
- this._getProjectContext(context.project)
301
- );
302
- }
303
-
304
- _getToolContext(tool = {}){
305
- let value = '';
306
- if (tool.name){
307
- value += this._toolIdent(tool);
308
- if (tool.components){
309
- for (let component of tool.components){
310
- value += ', '+this._toolIdent(component);
311
- }
312
- }
313
- }
314
- if (value){
315
- return { 'X-Particle-Tool': value };
316
- }
317
- return {};
318
- }
319
-
320
- _toolIdent(tool){
321
- return this._nameAtVersion(tool.name, tool.version);
322
- }
323
-
324
- _nameAtVersion(name, version){
325
- let value = '';
326
- if (name){
327
- value += name;
328
- if (version){
329
- value += '@'+version;
330
- }
331
- }
332
- return value;
333
- }
334
-
335
- _getProjectContext(project = {}){
336
- let value = this._buildSemicolonSeparatedProperties(project, 'name');
337
- if (value){
338
- return { 'X-Particle-Project': value };
339
- }
340
- return {};
341
- }
342
-
343
- /**
344
- * Creates a string like primaryPropertyValue; name=value; name1=value
345
- * from the properties of an object.
346
- * @param {object} obj The object to create the string from
347
- * @param {string} primaryProperty The name of the primary property which is the default value and must be defined.
348
- * @private
349
- * @return {string} The formatted string representing the object properties and the default property.
350
- */
351
- _buildSemicolonSeparatedProperties(obj, primaryProperty){
352
- let value = '';
353
- if (obj[primaryProperty]){
354
- value += obj[primaryProperty];
355
- for (let prop in obj){
356
- if (prop!==primaryProperty && obj.hasOwnProperty(prop)){
357
- value += '; '+prop+'='+obj[prop];
358
- }
359
- }
360
- }
361
- return value;
362
- }
363
-
364
- /**
365
- * Adds an authorization header.
366
- * @param {string|object} auth The authorization bearer token.
367
- * @returns {object} The original request.
368
- */
369
- _getAuthorizationHeader(auth){
370
- if (!auth) {
371
- return {};
372
- }
373
- if (typeof auth === 'string') {
374
- return { Authorization: `Bearer ${auth}` };
375
- }
376
- let encoded;
377
- if (this.isForBrowser()) {
378
- encoded = btoa(`${auth.username}:${auth.password}`);
379
- } else {
380
- encoded = Buffer.from(`${auth.username}:${auth.password}`)
381
- .toString('base64');
382
- }
383
- return { Authorization: `Basic ${encoded}` };
384
- }
385
-
386
- /**
387
- *
388
- * @param {Object} files converts the file names to file, file1, file2.
389
- * @returns {object} the renamed files.
390
- */
391
- _sanitizeFiles(files){
392
- let requestFiles;
393
- if (files){
394
- requestFiles = {};
395
- Object.keys(files).forEach((k, i) => {
396
- const name = i ? `file${i + 1}` : 'file';
397
- requestFiles[name] = {
398
- data: files[k],
399
- path: k
400
- };
401
- });
402
- }
403
- return requestFiles;
404
- }
64
+ constructor(baseUrl){
65
+ this.setBaseUrl(baseUrl);
66
+ }
67
+
68
+ setBaseUrl(baseUrl) {
69
+ this.baseUrl = baseUrl;
70
+ }
71
+
72
+ /**
73
+ * Make a GET request
74
+ * @param {object} params Configurations to customize the request
75
+ * @param {string} params.uri The URI to request
76
+ * @param {Auth} [params.auth] Authorization token to use
77
+ * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
78
+ * @param {object} [params.query] Key/Value pairs of query params
79
+ * @param {object} [params.context] The invocation context, describing the tool and project
80
+ * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
81
+ */
82
+ get({ uri, auth, headers, query, context }) {
83
+ return this.request({ uri, method: 'get', auth, headers, query, context });
84
+ }
85
+
86
+ /**
87
+ * Make a HEAD request
88
+ * @param {object} params Configurations to customize the request
89
+ * @param {string} params.uri The URI to request
90
+ * @param {Auth} [params.auth] Authorization token to use
91
+ * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
92
+ * @param {object} [params.query] Key/Value pairs of query params
93
+ * @param {object} [params.context] The invocation context, describing the tool and project
94
+ * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
95
+ */
96
+ head({ uri, auth, headers, query, context }) {
97
+ return this.request({ uri, method: 'head', auth, headers, query, context });
98
+ }
99
+
100
+ /**
101
+ * Make a POST request
102
+ * @param {object} params Configurations to customize the request
103
+ * @param {string} params.uri The URI to request
104
+ * @param {Auth} [params.auth] Authorization token to use
105
+ * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
106
+ * @param {object} [params.data] Key/Value pairs of query params
107
+ * @param {object} [params.context] The invocation context, describing the tool and project
108
+ * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
109
+ */
110
+ post({ uri, headers, data, auth, context }) {
111
+ return this.request({ uri, method: 'post', auth, headers, data, context });
112
+ }
113
+
114
+ /**
115
+ * Make a PUT request
116
+ * @param {object} params Configurations to customize the request
117
+ * @param {string} params.uri The URI to request
118
+ * @param {Auth} [params.auth] Authorization token to use
119
+ * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
120
+ * @param {object} [params.data] Key/VAlue pairs of query params
121
+ * @param {object} [params.context] The invocation context, describing the tool and project
122
+ * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
123
+ */
124
+ put({ uri, auth, headers, data, context }) {
125
+ return this.request({ uri, method: 'put', auth, headers, data, context });
126
+ }
127
+
128
+ /**
129
+ * Make a DELETE request
130
+ * @param {object} params Configurations to customize the request
131
+ * @param {string} params.uri The URI to request
132
+ * @param {Auth} [params.auth] Authorization token to use
133
+ * @param {object} [params.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
134
+ * @param {object} [params.data] Key/Value pairs of query params
135
+ * @param {object} [params.context] The invocation context, describing the tool and project
136
+ * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
137
+ */
138
+ delete({ uri, auth, headers, data, context }) {
139
+ return this.request({ uri, method: 'delete', auth, headers, data, context });
140
+ }
141
+
142
+ /**
143
+ *
144
+ * @param {object} config An obj with all the possible request configurations
145
+ * @param {string} config.uri The URI to request
146
+ * @param {string} config.method The method used to request the URI, should be in uppercase.
147
+ * @param {object} [config.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
148
+ * @param {object} [config.data] Arbitrary data to send as the body.
149
+ * @param {Auth} [config.auth] Authorization
150
+ * @param {object} [config.query] Query parameters
151
+ * @param {object} [config.form] Form fields
152
+ * @param {object} [config.files] Array of file names and file content
153
+ * @param {object} [config.context] The invocation context, describing the tool and project.
154
+ * @param {boolean} [config.isBuffer=false] Indicate if the response should be treated as Buffer instead of JSON
155
+ * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
156
+ */
157
+ request({
158
+ uri,
159
+ method,
160
+ headers = undefined,
161
+ data = undefined,
162
+ auth,
163
+ query = undefined,
164
+ form = undefined,
165
+ files = undefined,
166
+ context = undefined,
167
+ isBuffer = false
168
+ }){
169
+ const requestFiles = this._sanitizeFiles(files);
170
+ const requestParams = this._buildRequest({ uri, method, headers, data, auth, query, form, context, files: requestFiles });
171
+ return this._promiseResponse(requestParams, isBuffer);
172
+ }
173
+
174
+ /**
175
+ * Promises to send the request and retrieve the response.
176
+ * @param {[string, object]} requestParams First argument is the URI to request, the second one are the options.
177
+ * @param {boolean} isBuffer Indicate if the response body should be returned as a Buffer (Node) / ArrayBuffer (browser) instead of JSON
178
+ * @param {function} [makerequest=fetch] The fetch function to use. Override for testing.
179
+ * @returns {Promise<RequestResponse, RequestError>} A promise that resolves with either the requested data or an error object
180
+ * @private
181
+ */
182
+ _promiseResponse(requestParams, isBuffer, makerequest = fetch) {
183
+ let status;
184
+ return makerequest(...requestParams)
185
+ .then((resp) => {
186
+ status = resp.status;
187
+ if (!resp.ok) {
188
+ return resp.text().then((err) => {
189
+ const objError = JSON.parse(err);
190
+ // particle-commnds/src/cmd/api expects response.text. to be a string
191
+ const response = Object.assign(resp, { text: err });
192
+ throw Object.assign(objError, { response });
193
+ });
194
+ }
195
+ if (status === 204) { // Can't do resp.json() since there is no body to parse
196
+ return '';
197
+ }
198
+ if (isBuffer) {
199
+ return resp.blob();
200
+ }
201
+ return resp.json();
202
+ }).then((body) => {
203
+ if (isBuffer) {
204
+ return body.arrayBuffer().then((arrayBuffer) => {
205
+ if (!this.isForBrowser()) {
206
+ return Buffer.from(arrayBuffer);
207
+ }
208
+ return arrayBuffer;
209
+ });
210
+ }
211
+ return {
212
+ body,
213
+ statusCode: status
214
+ };
215
+ }).catch((error) => {
216
+ const errorType = status ? `HTTP error ${status}` : 'Network error';
217
+ let errorDescription = `${errorType} from ${requestParams[0]}`;
218
+ let shortErrorDescription;
219
+ if (error.error_description) { // Fetch responded with ok false
220
+ errorDescription = `${errorDescription} - ${error.error_description}`;
221
+ shortErrorDescription = error.error_description;
222
+ }
223
+ const reason = new Error(errorDescription);
224
+ Object.assign(reason, {
225
+ statusCode: status,
226
+ errorDescription,
227
+ shortErrorDescription,
228
+ error,
229
+ body: error
230
+ });
231
+ throw reason;
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Generate the params in a format valid for 'fetch'
237
+ * @param {object} config Configurations to customize the request
238
+ * @param {string} config.uri The URI to request
239
+ * @param {string} config.method The method used to request the URI, should be in uppercase.
240
+ * @param {object} [config.headers] Key/Value pairs like `{ 'X-FOO': 'foo', X-BAR: 'bar' }` to send as headers.
241
+ * @param {object} [config.data] Arbitrary data to send as the body.
242
+ * @param {Auth} [config.auth] Authorization
243
+ * @param {object} [config.query] Query parameters
244
+ * @param {object} [config.form] Form fields
245
+ * @param {object} [config.files] Array of file names and file content
246
+ * @param {object} [config.context] The invocation context, describing the tool and project.
247
+ * @returns {[string, object]} The uri to make the request too, and extra configs
248
+ * @private
249
+ */
250
+ _buildRequest({ uri, method, headers, data, auth, query, form, files, context }){
251
+ let actualUri = uri;
252
+ if (this.baseUrl && uri[0] === '/') {
253
+ actualUri = `${this.baseUrl}${uri}`;
254
+ }
255
+ if (query) {
256
+ const queryParams = qs.stringify(query);
257
+ const hasParams = actualUri.includes('?');
258
+ actualUri = `${actualUri}${hasParams ? '&' : '?'}${queryParams}`;
259
+ }
260
+
261
+ const userAgentHeader = { 'User-Agent': `${packageJson.name}/${packageJson.version} (${packageJson.repository.url})` };
262
+ let body;
263
+ let contentTypeHeader;
264
+ if (files){
265
+ // @ts-ignore
266
+ contentTypeHeader = {}; // Needed to allow fetch create its own
267
+ body = this._getFromData(files, form);
268
+ } else if (form){
269
+ contentTypeHeader = { 'Content-Type': 'application/x-www-form-urlencoded' };
270
+ body = qs.stringify(form);
271
+ } else if (data){
272
+ contentTypeHeader = { 'Content-Type': 'application/json' };
273
+ body = JSON.stringify(data);
274
+ }
275
+ const finalHeaders = Object.assign({},
276
+ userAgentHeader,
277
+ contentTypeHeader,
278
+ this._getAuthorizationHeader(auth),
279
+ this._getContextHeaders(context),
280
+ headers
281
+ );
282
+
283
+ return [actualUri, { method, body, headers: finalHeaders }];
284
+ }
285
+
286
+ isForBrowser() {
287
+ return typeof window !== 'undefined';
288
+ }
289
+
290
+ _getFromData(files, form) {
291
+ const formData = new FormData();
292
+ for (let [name, file] of Object.entries(files)){
293
+ let path = file.path;
294
+ let fileData = file.data;
295
+ if (!this.isForBrowser()) {
296
+ const nodeFormData = this._getNodeFormData(file);
297
+ path = nodeFormData.path;
298
+ fileData = nodeFormData.file;
299
+ }
300
+ formData.append(name, fileData, path);
301
+ }
302
+ if (form){
303
+ for (let [name, value] of Object.entries(form)){
304
+ formData.append(name, value);
305
+ }
306
+ }
307
+ return formData;
308
+ }
309
+
310
+ _getNodeFormData(file) {
311
+ let fileData = file.data;
312
+ if (typeof file.data === 'string') {
313
+ fileData = fs.createReadStream(file.data);
314
+ }
315
+ return {
316
+ file: fileData,
317
+ path: { filepath: file.path } // Different API for nodejs
318
+ };
319
+ }
320
+
321
+ _getContextHeaders(context = {}) {
322
+ return Object.assign({},
323
+ this._getToolContext(context.tool),
324
+ this._getProjectContext(context.project)
325
+ );
326
+ }
327
+
328
+ _getToolContext(tool = {}){
329
+ let value = '';
330
+ if (tool.name){
331
+ value += this._toolIdent(tool);
332
+ if (tool.components){
333
+ for (let component of tool.components){
334
+ value += ', '+this._toolIdent(component);
335
+ }
336
+ }
337
+ }
338
+ if (value){
339
+ return { 'X-Particle-Tool': value };
340
+ }
341
+ return {};
342
+ }
343
+
344
+ _toolIdent(tool){
345
+ return this._nameAtVersion(tool.name, tool.version);
346
+ }
347
+
348
+ _nameAtVersion(name, version){
349
+ let value = '';
350
+ if (name){
351
+ value += name;
352
+ if (version){
353
+ value += '@'+version;
354
+ }
355
+ }
356
+ return value;
357
+ }
358
+
359
+ _getProjectContext(project = {}){
360
+ let value = this._buildSemicolonSeparatedProperties(project, 'name');
361
+ if (value){
362
+ return { 'X-Particle-Project': value };
363
+ }
364
+ return {};
365
+ }
366
+
367
+ /**
368
+ * Creates a string like primaryPropertyValue; name=value; name1=value
369
+ * from the properties of an object.
370
+ * @param {object} obj The object to create the string from
371
+ * @param {string} primaryProperty The name of the primary property which is the default value and must be defined.
372
+ * @private
373
+ * @return {string} The formatted string representing the object properties and the default property.
374
+ */
375
+ _buildSemicolonSeparatedProperties(obj, primaryProperty){
376
+ let value = '';
377
+ if (obj[primaryProperty]){
378
+ value += obj[primaryProperty];
379
+ for (let prop in obj){
380
+ if (prop!==primaryProperty && obj.hasOwnProperty(prop)){
381
+ value += '; '+prop+'='+obj[prop];
382
+ }
383
+ }
384
+ }
385
+ return value;
386
+ }
387
+
388
+ /**
389
+ * Adds an authorization header.
390
+ * @param {Auth} [auth] The authorization bearer token.
391
+ * @returns {object} The original request.
392
+ */
393
+ _getAuthorizationHeader(auth){
394
+ if (!auth) {
395
+ return {};
396
+ }
397
+ if (typeof auth === 'string') {
398
+ return { Authorization: `Bearer ${auth}` };
399
+ }
400
+ let encoded;
401
+ if (this.isForBrowser()) {
402
+ encoded = btoa(`${auth.username}:${auth.password}`);
403
+ } else {
404
+ encoded = Buffer.from(`${auth.username}:${auth.password}`)
405
+ .toString('base64');
406
+ }
407
+ return { Authorization: `Basic ${encoded}` };
408
+ }
409
+
410
+ /**
411
+ *
412
+ * @param {Object} files converts the file names to file, file1, file2.
413
+ * @returns {object} the renamed files.
414
+ */
415
+ _sanitizeFiles(files){
416
+ let requestFiles;
417
+ if (files){
418
+ requestFiles = {};
419
+ Object.keys(files).forEach((k, i) => {
420
+ const name = i ? `file${i + 1}` : 'file';
421
+ requestFiles[name] = {
422
+ data: files[k],
423
+ path: k
424
+ };
425
+ });
426
+ }
427
+ return requestFiles;
428
+ }
405
429
  }
406
430
 
407
431
  module.exports = Agent;