@devicecloud.dev/dcd 4.2.0 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApiGateway = void 0;
4
4
  const path = require("node:path");
5
5
  exports.ApiGateway = {
6
+ /**
7
+ * Enhances generic "fetch failed" errors with more specific diagnostic information
8
+ * @param error - The original TypeError from fetch
9
+ * @param url - The URL that was being fetched
10
+ * @returns Enhanced error with diagnostic information
11
+ */
12
+ enhanceFetchError(error, url) {
13
+ const urlObj = new URL(url);
14
+ const { hostname, origin } = urlObj;
15
+ let message = `Network request failed: ${url}\n\n`;
16
+ message += `Possible causes:\n`;
17
+ message += ` 1. No internet connection - check your network connectivity\n`;
18
+ message += ` 2. DNS resolution failed - unable to resolve "${hostname}"\n`;
19
+ message += ` 3. Firewall or proxy blocking the request\n`;
20
+ message += ` 4. API server is down or unreachable\n`;
21
+ message += ` 5. SSL/TLS certificate validation failed\n\n`;
22
+ message += `Troubleshooting steps:\n`;
23
+ message += ` • Check internet connection: ping google.com\n`;
24
+ message += ` • Test API reachability: curl ${origin}\n`;
25
+ message += ` • Verify API URL is correct: ${origin}\n`;
26
+ message += ` • Check for proxy/VPN interference\n`;
27
+ message += ` • Try again in a few moments if server is temporarily down\n\n`;
28
+ message += `Original error: ${error.message}`;
29
+ const enhancedError = new Error(message);
30
+ enhancedError.name = 'NetworkError';
31
+ enhancedError.stack = error.stack;
32
+ return enhancedError;
33
+ },
6
34
  /**
7
35
  * Standardized error handling for API responses
8
36
  * @param res - The fetch response object
@@ -55,117 +83,169 @@ exports.ApiGateway = {
55
83
  }
56
84
  },
57
85
  async checkForExistingUpload(baseUrl, apiKey, sha) {
58
- const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
59
- body: JSON.stringify({ sha }),
60
- headers: {
61
- 'content-type': 'application/json',
62
- 'x-app-api-key': apiKey,
63
- },
64
- method: 'POST',
65
- });
66
- return res.json();
67
- },
68
- async downloadArtifactsZip(baseUrl, apiKey, uploadId, results, artifactsPath = './artifacts.zip') {
69
- const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
70
- body: JSON.stringify({ results }),
71
- headers: {
72
- 'content-type': 'application/json',
73
- 'x-app-api-key': apiKey,
74
- },
75
- method: 'POST',
76
- });
77
- if (!res.ok) {
78
- await this.handleApiError(res, 'Failed to download artifacts');
86
+ try {
87
+ const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
88
+ body: JSON.stringify({ sha }),
89
+ headers: {
90
+ 'content-type': 'application/json',
91
+ 'x-app-api-key': apiKey,
92
+ },
93
+ method: 'POST',
94
+ });
95
+ if (!res.ok) {
96
+ await this.handleApiError(res, 'Failed to check for existing upload');
97
+ }
98
+ return res.json();
79
99
  }
80
- // Handle tilde expansion for home directory
81
- if (artifactsPath.startsWith('~/') || artifactsPath === '~') {
82
- artifactsPath = artifactsPath.replace(/^~(?=$|\/|\\)/,
83
- // eslint-disable-next-line unicorn/prefer-module
84
- require('node:os').homedir());
100
+ catch (error) {
101
+ // Handle network-level errors (DNS, connection refused, timeout, etc.)
102
+ if (error instanceof TypeError && error.message === 'fetch failed') {
103
+ throw this.enhanceFetchError(error, `${baseUrl}/uploads/checkForExistingUpload`);
104
+ }
105
+ throw error;
85
106
  }
86
- // Create directory structure if it doesn't exist
87
- // eslint-disable-next-line unicorn/prefer-module
88
- const { dirname } = require('node:path');
89
- // eslint-disable-next-line unicorn/prefer-module
90
- const { createWriteStream, mkdirSync } = require('node:fs');
91
- // eslint-disable-next-line unicorn/prefer-module
92
- const { finished } = require('node:stream/promises');
93
- // eslint-disable-next-line unicorn/prefer-module
94
- const { Readable } = require('node:stream');
95
- const directory = dirname(artifactsPath);
96
- if (directory !== '.') {
97
- try {
98
- mkdirSync(directory, { recursive: true });
107
+ },
108
+ async downloadArtifactsZip(baseUrl, apiKey, uploadId, results, artifactsPath = './artifacts.zip') {
109
+ try {
110
+ const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
111
+ body: JSON.stringify({ results }),
112
+ headers: {
113
+ 'content-type': 'application/json',
114
+ 'x-app-api-key': apiKey,
115
+ },
116
+ method: 'POST',
117
+ });
118
+ if (!res.ok) {
119
+ await this.handleApiError(res, 'Failed to download artifacts');
120
+ }
121
+ // Handle tilde expansion for home directory
122
+ if (artifactsPath.startsWith('~/') || artifactsPath === '~') {
123
+ artifactsPath = artifactsPath.replace(/^~(?=$|\/|\\)/,
124
+ // eslint-disable-next-line unicorn/prefer-module
125
+ require('node:os').homedir());
99
126
  }
100
- catch (error) {
101
- // Ignore if directory already exists
102
- if (error.code !== 'EEXIST') {
103
- throw error;
127
+ // Create directory structure if it doesn't exist
128
+ // eslint-disable-next-line unicorn/prefer-module
129
+ const { dirname } = require('node:path');
130
+ // eslint-disable-next-line unicorn/prefer-module
131
+ const { createWriteStream, mkdirSync } = require('node:fs');
132
+ // eslint-disable-next-line unicorn/prefer-module
133
+ const { finished } = require('node:stream/promises');
134
+ // eslint-disable-next-line unicorn/prefer-module
135
+ const { Readable } = require('node:stream');
136
+ const directory = dirname(artifactsPath);
137
+ if (directory !== '.') {
138
+ try {
139
+ mkdirSync(directory, { recursive: true });
140
+ }
141
+ catch (error) {
142
+ // Ignore if directory already exists
143
+ if (error.code !== 'EEXIST') {
144
+ throw error;
145
+ }
104
146
  }
105
147
  }
148
+ const fileStream = createWriteStream(artifactsPath, { flags: 'w' });
149
+ await finished(Readable.fromWeb(res.body).pipe(fileStream));
150
+ }
151
+ catch (error) {
152
+ if (error instanceof TypeError && error.message === 'fetch failed') {
153
+ throw this.enhanceFetchError(error, `${baseUrl}/results/${uploadId}/download`);
154
+ }
155
+ throw error;
106
156
  }
107
- const fileStream = createWriteStream(artifactsPath, { flags: 'w' });
108
- await finished(Readable.fromWeb(res.body).pipe(fileStream));
109
157
  },
110
158
  async finaliseUpload(config) {
111
159
  const { baseUrl, apiKey, id, metadata, path, sha, supabaseSuccess, backblazeSuccess } = config;
112
- const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
113
- body: JSON.stringify({
114
- backblazeSuccess,
115
- id,
116
- metadata,
117
- path, // This is tempPath for TUS uploads
118
- sha,
119
- supabaseSuccess,
120
- }),
121
- headers: {
122
- 'content-type': 'application/json',
123
- 'x-app-api-key': apiKey,
124
- },
125
- method: 'POST',
126
- });
127
- if (!res.ok) {
128
- await this.handleApiError(res, 'Failed to finalize upload');
160
+ try {
161
+ const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
162
+ body: JSON.stringify({
163
+ backblazeSuccess,
164
+ id,
165
+ metadata,
166
+ path, // This is tempPath for TUS uploads
167
+ sha,
168
+ supabaseSuccess,
169
+ }),
170
+ headers: {
171
+ 'content-type': 'application/json',
172
+ 'x-app-api-key': apiKey,
173
+ },
174
+ method: 'POST',
175
+ });
176
+ if (!res.ok) {
177
+ await this.handleApiError(res, 'Failed to finalize upload');
178
+ }
179
+ return res.json();
180
+ }
181
+ catch (error) {
182
+ if (error instanceof TypeError && error.message === 'fetch failed') {
183
+ throw this.enhanceFetchError(error, `${baseUrl}/uploads/finaliseUpload`);
184
+ }
185
+ throw error;
129
186
  }
130
- return res.json();
131
187
  },
132
188
  async getBinaryUploadUrl(baseUrl, apiKey, platform, fileSize) {
133
- const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
134
- body: JSON.stringify({ platform, fileSize }),
135
- headers: {
136
- 'content-type': 'application/json',
137
- 'x-app-api-key': apiKey,
138
- },
139
- method: 'POST',
140
- });
141
- if (!res.ok) {
142
- await this.handleApiError(res, 'Failed to get upload URL');
189
+ try {
190
+ const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
191
+ body: JSON.stringify({ platform, fileSize }),
192
+ headers: {
193
+ 'content-type': 'application/json',
194
+ 'x-app-api-key': apiKey,
195
+ },
196
+ method: 'POST',
197
+ });
198
+ if (!res.ok) {
199
+ await this.handleApiError(res, 'Failed to get upload URL');
200
+ }
201
+ return res.json();
202
+ }
203
+ catch (error) {
204
+ if (error instanceof TypeError && error.message === 'fetch failed') {
205
+ throw this.enhanceFetchError(error, `${baseUrl}/uploads/getBinaryUploadUrl`);
206
+ }
207
+ throw error;
143
208
  }
144
- return res.json();
145
209
  },
146
210
  async finishLargeFile(baseUrl, apiKey, fileId, partSha1Array) {
147
- const res = await fetch(`${baseUrl}/uploads/finishLargeFile`, {
148
- body: JSON.stringify({ fileId, partSha1Array }),
149
- headers: {
150
- 'content-type': 'application/json',
151
- 'x-app-api-key': apiKey,
152
- },
153
- method: 'POST',
154
- });
155
- if (!res.ok) {
156
- await this.handleApiError(res, 'Failed to finish large file');
211
+ try {
212
+ const res = await fetch(`${baseUrl}/uploads/finishLargeFile`, {
213
+ body: JSON.stringify({ fileId, partSha1Array }),
214
+ headers: {
215
+ 'content-type': 'application/json',
216
+ 'x-app-api-key': apiKey,
217
+ },
218
+ method: 'POST',
219
+ });
220
+ if (!res.ok) {
221
+ await this.handleApiError(res, 'Failed to finish large file');
222
+ }
223
+ return res.json();
224
+ }
225
+ catch (error) {
226
+ if (error instanceof TypeError && error.message === 'fetch failed') {
227
+ throw this.enhanceFetchError(error, `${baseUrl}/uploads/finishLargeFile`);
228
+ }
229
+ throw error;
157
230
  }
158
- return res.json();
159
231
  },
160
232
  async getResultsForUpload(baseUrl, apiKey, uploadId) {
161
233
  // TODO: merge with getUploadStatus
162
- const res = await fetch(`${baseUrl}/results/${uploadId}`, {
163
- headers: { 'x-app-api-key': apiKey },
164
- });
165
- if (!res.ok) {
166
- await this.handleApiError(res, 'Failed to get results');
234
+ try {
235
+ const res = await fetch(`${baseUrl}/results/${uploadId}`, {
236
+ headers: { 'x-app-api-key': apiKey },
237
+ });
238
+ if (!res.ok) {
239
+ await this.handleApiError(res, 'Failed to get results');
240
+ }
241
+ return res.json();
242
+ }
243
+ catch (error) {
244
+ if (error instanceof TypeError && error.message === 'fetch failed') {
245
+ throw this.enhanceFetchError(error, `${baseUrl}/results/${uploadId}`);
246
+ }
247
+ throw error;
167
248
  }
168
- return res.json();
169
249
  },
170
250
  async getUploadStatus(baseUrl, apiKey, options) {
171
251
  const queryParams = new URLSearchParams();
@@ -175,28 +255,80 @@ exports.ApiGateway = {
175
255
  if (options.name) {
176
256
  queryParams.append('name', options.name);
177
257
  }
178
- const response = await fetch(`${baseUrl}/uploads/status?${queryParams}`, {
179
- headers: {
180
- 'x-app-api-key': apiKey,
181
- },
182
- });
183
- if (!response.ok) {
184
- await this.handleApiError(response, 'Failed to get upload status');
258
+ try {
259
+ const response = await fetch(`${baseUrl}/uploads/status?${queryParams}`, {
260
+ headers: {
261
+ 'x-app-api-key': apiKey,
262
+ },
263
+ });
264
+ if (!response.ok) {
265
+ await this.handleApiError(response, 'Failed to get upload status');
266
+ }
267
+ return response.json();
268
+ }
269
+ catch (error) {
270
+ if (error instanceof TypeError && error.message === 'fetch failed') {
271
+ throw this.enhanceFetchError(error, `${baseUrl}/uploads/status?${queryParams}`);
272
+ }
273
+ throw error;
274
+ }
275
+ },
276
+ async listUploads(baseUrl, apiKey, options = {}) {
277
+ const queryParams = new URLSearchParams();
278
+ if (options.name) {
279
+ queryParams.append('name', options.name);
280
+ }
281
+ if (options.from) {
282
+ queryParams.append('from', options.from);
283
+ }
284
+ if (options.to) {
285
+ queryParams.append('to', options.to);
286
+ }
287
+ if (options.limit !== undefined) {
288
+ queryParams.append('limit', String(options.limit));
289
+ }
290
+ if (options.offset !== undefined) {
291
+ queryParams.append('offset', String(options.offset));
292
+ }
293
+ const url = `${baseUrl}/uploads/list?${queryParams}`;
294
+ try {
295
+ const response = await fetch(url, {
296
+ headers: {
297
+ 'x-app-api-key': apiKey,
298
+ },
299
+ });
300
+ if (!response.ok) {
301
+ await this.handleApiError(response, 'Failed to list uploads');
302
+ }
303
+ return response.json();
304
+ }
305
+ catch (error) {
306
+ if (error instanceof TypeError && error.message === 'fetch failed') {
307
+ throw this.enhanceFetchError(error, url);
308
+ }
309
+ throw error;
185
310
  }
186
- return response.json();
187
311
  },
188
312
  async uploadFlow(baseUrl, apiKey, testFormData) {
189
- const res = await fetch(`${baseUrl}/uploads/flow`, {
190
- body: testFormData,
191
- headers: {
192
- 'x-app-api-key': apiKey,
193
- },
194
- method: 'POST',
195
- });
196
- if (!res.ok) {
197
- await this.handleApiError(res, 'Failed to upload test flows');
313
+ try {
314
+ const res = await fetch(`${baseUrl}/uploads/flow`, {
315
+ body: testFormData,
316
+ headers: {
317
+ 'x-app-api-key': apiKey,
318
+ },
319
+ method: 'POST',
320
+ });
321
+ if (!res.ok) {
322
+ await this.handleApiError(res, 'Failed to upload test flows');
323
+ }
324
+ return res.json();
325
+ }
326
+ catch (error) {
327
+ if (error instanceof TypeError && error.message === 'fetch failed') {
328
+ throw this.enhanceFetchError(error, `${baseUrl}/uploads/flow`);
329
+ }
330
+ throw error;
198
331
  }
199
- return res.json();
200
332
  },
201
333
  /**
202
334
  * Generic report download method that handles both junit and allure reports
@@ -232,51 +364,59 @@ exports.ApiGateway = {
232
364
  const { endpoint, defaultFilename, notFoundMessage, errorPrefix } = config[reportType];
233
365
  const finalReportPath = reportPath || path.resolve(process.cwd(), defaultFilename);
234
366
  const url = `${baseUrl}${endpoint}`;
235
- // Make the download request
236
- const res = await fetch(url, {
237
- headers: {
238
- 'x-app-api-key': apiKey,
239
- },
240
- method: 'GET',
241
- });
242
- if (!res.ok) {
243
- const errorText = await res.text();
244
- if (res.status === 404) {
245
- throw new Error(notFoundMessage);
367
+ try {
368
+ // Make the download request
369
+ const res = await fetch(url, {
370
+ headers: {
371
+ 'x-app-api-key': apiKey,
372
+ },
373
+ method: 'GET',
374
+ });
375
+ if (!res.ok) {
376
+ const errorText = await res.text();
377
+ if (res.status === 404) {
378
+ throw new Error(notFoundMessage);
379
+ }
380
+ throw new Error(`${errorPrefix}: ${res.status} ${errorText}`);
246
381
  }
247
- throw new Error(`${errorPrefix}: ${res.status} ${errorText}`);
248
- }
249
- // Handle tilde expansion for home directory (applies to all report types)
250
- let expandedPath = finalReportPath;
251
- if (finalReportPath.startsWith('~/') || finalReportPath === '~') {
252
- expandedPath = finalReportPath.replace(/^~/,
253
- // eslint-disable-next-line unicorn/prefer-module
254
- require('node:os').homedir());
255
- }
256
- // Create directory structure if it doesn't exist
257
- // eslint-disable-next-line unicorn/prefer-module
258
- const { dirname } = require('node:path');
259
- // eslint-disable-next-line unicorn/prefer-module
260
- const { createWriteStream, mkdirSync } = require('node:fs');
261
- // eslint-disable-next-line unicorn/prefer-module
262
- const { finished } = require('node:stream/promises');
263
- // eslint-disable-next-line unicorn/prefer-module
264
- const { Readable } = require('node:stream');
265
- const directory = dirname(expandedPath);
266
- if (directory !== '.') {
267
- try {
268
- mkdirSync(directory, { recursive: true });
382
+ // Handle tilde expansion for home directory (applies to all report types)
383
+ let expandedPath = finalReportPath;
384
+ if (finalReportPath.startsWith('~/') || finalReportPath === '~') {
385
+ expandedPath = finalReportPath.replace(/^~/,
386
+ // eslint-disable-next-line unicorn/prefer-module
387
+ require('node:os').homedir());
269
388
  }
270
- catch (error) {
271
- // Ignore EEXIST errors (directory already exists)
272
- if (error.code !== 'EEXIST') {
273
- throw error;
389
+ // Create directory structure if it doesn't exist
390
+ // eslint-disable-next-line unicorn/prefer-module
391
+ const { dirname } = require('node:path');
392
+ // eslint-disable-next-line unicorn/prefer-module
393
+ const { createWriteStream, mkdirSync } = require('node:fs');
394
+ // eslint-disable-next-line unicorn/prefer-module
395
+ const { finished } = require('node:stream/promises');
396
+ // eslint-disable-next-line unicorn/prefer-module
397
+ const { Readable } = require('node:stream');
398
+ const directory = dirname(expandedPath);
399
+ if (directory !== '.') {
400
+ try {
401
+ mkdirSync(directory, { recursive: true });
402
+ }
403
+ catch (error) {
404
+ // Ignore EEXIST errors (directory already exists)
405
+ if (error.code !== 'EEXIST') {
406
+ throw error;
407
+ }
274
408
  }
275
409
  }
410
+ // Write the file using streaming for better memory efficiency
411
+ // Use 'w' flag to overwrite existing files instead of failing
412
+ const fileStream = createWriteStream(expandedPath, { flags: 'w' });
413
+ await finished(Readable.fromWeb(res.body).pipe(fileStream));
414
+ }
415
+ catch (error) {
416
+ if (error instanceof TypeError && error.message === 'fetch failed') {
417
+ throw this.enhanceFetchError(error, url);
418
+ }
419
+ throw error;
276
420
  }
277
- // Write the file using streaming for better memory efficiency
278
- // Use 'w' flag to overwrite existing files instead of failing
279
- const fileStream = createWriteStream(expandedPath, { flags: 'w' });
280
- await finished(Readable.fromWeb(res.body).pipe(fileStream));
281
421
  },
282
422
  };