@devicecloud.dev/dcd 4.1.6 → 4.1.9-beta.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/dist/commands/cloud.d.ts +0 -1
- package/dist/commands/cloud.js +45 -39
- package/dist/commands/upload.js +8 -1
- package/dist/config/flags/device.flags.d.ts +0 -1
- package/dist/config/flags/device.flags.js +0 -4
- package/dist/constants.d.ts +0 -1
- package/dist/gateways/api-gateway.d.ts +15 -4
- package/dist/gateways/api-gateway.js +35 -5
- package/dist/gateways/supabase-gateway.d.ts +28 -0
- package/dist/gateways/supabase-gateway.js +159 -19
- package/dist/methods.d.ts +10 -1
- package/dist/methods.js +386 -32
- package/dist/services/device-validation.service.js +7 -7
- package/dist/services/moropo.service.js +6 -6
- package/dist/services/report-download.service.js +4 -4
- package/dist/services/results-polling.service.js +13 -13
- package/dist/services/test-submission.service.d.ts +0 -1
- package/dist/services/test-submission.service.js +8 -9
- package/dist/services/version.service.js +4 -4
- package/dist/types/generated/schema.types.d.ts +117 -8
- package/dist/types/schema.types.d.ts +113 -6
- package/oclif.manifest.json +1 -7
- package/package.json +4 -2
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -55,7 +55,6 @@ export default class Cloud extends Command {
|
|
|
55
55
|
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
56
56
|
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
57
57
|
'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
58
|
-
'skip-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
59
58
|
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
60
59
|
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
61
60
|
'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -100,10 +100,10 @@ class Cloud extends core_1.Command {
|
|
|
100
100
|
debugFlag = debug === true;
|
|
101
101
|
jsonFile = flags['json-file'] === true;
|
|
102
102
|
if (debug) {
|
|
103
|
-
this.log('DEBUG
|
|
104
|
-
this.log(`DEBUG
|
|
105
|
-
this.log(`DEBUG
|
|
106
|
-
this.log(`DEBUG
|
|
103
|
+
this.log('[DEBUG] Starting command execution with debug logging enabled');
|
|
104
|
+
this.log(`[DEBUG] CLI Version: ${this.config.version}`);
|
|
105
|
+
this.log(`[DEBUG] Node version: ${process.versions.node}`);
|
|
106
|
+
this.log(`[DEBUG] OS: ${process.platform} ${process.arch}`);
|
|
107
107
|
}
|
|
108
108
|
if (flags['json-file']) {
|
|
109
109
|
quiet = true;
|
|
@@ -146,18 +146,18 @@ class Cloud extends core_1.Command {
|
|
|
146
146
|
try {
|
|
147
147
|
compatibilityData = await (0, compatibility_1.fetchCompatibilityData)(apiUrl, apiKey);
|
|
148
148
|
if (debug) {
|
|
149
|
-
this.log('DEBUG
|
|
149
|
+
this.log('[DEBUG] Successfully fetched compatibility data from API');
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
catch (error) {
|
|
153
153
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
154
|
if (debug) {
|
|
155
|
-
this.log(`DEBUG
|
|
155
|
+
this.log(`[DEBUG] Failed to fetch compatibility data from API: ${errorMessage}`);
|
|
156
156
|
}
|
|
157
157
|
throw new Error(`Failed to fetch device compatibility data: ${errorMessage}. Please check your API key and connection.`);
|
|
158
158
|
}
|
|
159
159
|
if (debug) {
|
|
160
|
-
this.log(`DEBUG
|
|
160
|
+
this.log(`[DEBUG] API URL: ${apiUrl}`);
|
|
161
161
|
}
|
|
162
162
|
// Resolve and validate Maestro version using API data
|
|
163
163
|
const resolvedMaestroVersion = this.versionService.resolveMaestroVersion(maestroVersion, compatibilityData, {
|
|
@@ -176,18 +176,18 @@ class Cloud extends core_1.Command {
|
|
|
176
176
|
this.log(styling_1.colors.info('ℹ') + ' ' + styling_1.colors.dim('Note: runnerType m1 is experimental and currently supports Android (Pixel 7, API Level 34) only.'));
|
|
177
177
|
}
|
|
178
178
|
if (runnerType === 'gpu1') {
|
|
179
|
-
this.log(styling_1.colors.info('ℹ') + ' ' + styling_1.colors.dim('Note: runnerType gpu1 is Android-only
|
|
179
|
+
this.log(styling_1.colors.info('ℹ') + ' ' + styling_1.colors.dim('Note: runnerType gpu1 is Android-only (Pixel 7, API Level 34 or 35), available to all users.'));
|
|
180
180
|
}
|
|
181
181
|
const { firstFile, secondFile } = args;
|
|
182
182
|
let finalBinaryId = appBinaryId;
|
|
183
183
|
const finalAppFile = appFile ?? firstFile;
|
|
184
184
|
let flowFile = flows ?? secondFile;
|
|
185
185
|
if (debug) {
|
|
186
|
-
this.log(`DEBUG
|
|
187
|
-
this.log(`DEBUG
|
|
188
|
-
this.log(`DEBUG
|
|
189
|
-
this.log(`DEBUG
|
|
190
|
-
this.log(`DEBUG
|
|
186
|
+
this.log(`[DEBUG] First file argument: ${firstFile || 'not provided'}`);
|
|
187
|
+
this.log(`[DEBUG] Second file argument: ${secondFile || 'not provided'}`);
|
|
188
|
+
this.log(`[DEBUG] App binary ID: ${appBinaryId || 'not provided'}`);
|
|
189
|
+
this.log(`[DEBUG] App file: ${finalAppFile || 'not provided'}`);
|
|
190
|
+
this.log(`[DEBUG] Flow file: ${flowFile || 'not provided'}`);
|
|
191
191
|
}
|
|
192
192
|
if (appBinaryId) {
|
|
193
193
|
if (secondFile) {
|
|
@@ -209,12 +209,12 @@ class Cloud extends core_1.Command {
|
|
|
209
209
|
flowFile += '/';
|
|
210
210
|
}
|
|
211
211
|
if (debug) {
|
|
212
|
-
this.log(`DEBUG
|
|
212
|
+
this.log(`[DEBUG] Resolved flow file path: ${flowFile}`);
|
|
213
213
|
}
|
|
214
214
|
let executionPlan;
|
|
215
215
|
try {
|
|
216
216
|
if (debug) {
|
|
217
|
-
this.log('DEBUG
|
|
217
|
+
this.log('[DEBUG] Generating execution plan...');
|
|
218
218
|
}
|
|
219
219
|
executionPlan = await (0, execution_plan_service_1.plan)({
|
|
220
220
|
input: flowFile,
|
|
@@ -225,24 +225,24 @@ class Cloud extends core_1.Command {
|
|
|
225
225
|
debug,
|
|
226
226
|
});
|
|
227
227
|
if (debug) {
|
|
228
|
-
this.log(`DEBUG
|
|
229
|
-
this.log(`DEBUG
|
|
230
|
-
this.log(`DEBUG
|
|
231
|
-
this.log(`DEBUG
|
|
232
|
-
this.log(`DEBUG
|
|
228
|
+
this.log(`[DEBUG] Execution plan generated`);
|
|
229
|
+
this.log(`[DEBUG] Total flow files: ${executionPlan.totalFlowFiles}`);
|
|
230
|
+
this.log(`[DEBUG] Flows to run: ${executionPlan.flowsToRun.length}`);
|
|
231
|
+
this.log(`[DEBUG] Referenced files: ${executionPlan.referencedFiles.length}`);
|
|
232
|
+
this.log(`[DEBUG] Sequential flows: ${executionPlan.sequence?.flows.length || 0}`);
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
catch (error) {
|
|
236
236
|
if (debug) {
|
|
237
|
-
this.log(`DEBUG
|
|
237
|
+
this.log(`[DEBUG] Error generating execution plan: ${error}`);
|
|
238
238
|
}
|
|
239
239
|
throw error;
|
|
240
240
|
}
|
|
241
241
|
const { allExcludeTags, allIncludeTags, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
242
242
|
if (debug) {
|
|
243
|
-
this.log(`DEBUG
|
|
244
|
-
this.log(`DEBUG
|
|
245
|
-
this.log(`DEBUG
|
|
243
|
+
this.log(`[DEBUG] All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
|
|
244
|
+
this.log(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
|
|
245
|
+
this.log(`[DEBUG] Test file names: ${testFileNames.join(', ')}`);
|
|
246
246
|
}
|
|
247
247
|
const pathsShortestToLongest = [
|
|
248
248
|
...testFileNames,
|
|
@@ -257,12 +257,12 @@ class Cloud extends core_1.Command {
|
|
|
257
257
|
commonRoot = folderPath;
|
|
258
258
|
}
|
|
259
259
|
if (debug) {
|
|
260
|
-
this.log(`DEBUG
|
|
260
|
+
this.log(`[DEBUG] Common root directory: ${commonRoot}`);
|
|
261
261
|
}
|
|
262
262
|
const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
|
|
263
263
|
if (debug && sequentialFlows.length > 0) {
|
|
264
|
-
this.log(`DEBUG
|
|
265
|
-
this.log(`DEBUG
|
|
264
|
+
this.log(`[DEBUG] Sequential flows: ${sequentialFlows.join(', ')}`);
|
|
265
|
+
this.log(`[DEBUG] Continue on failure: ${continueOnFailure}`);
|
|
266
266
|
}
|
|
267
267
|
if (!appBinaryId) {
|
|
268
268
|
if (!(flowFile && finalAppFile)) {
|
|
@@ -273,7 +273,7 @@ class Cloud extends core_1.Command {
|
|
|
273
273
|
}
|
|
274
274
|
if (finalAppFile.endsWith('.zip')) {
|
|
275
275
|
if (debug) {
|
|
276
|
-
this.log(`DEBUG
|
|
276
|
+
this.log(`[DEBUG] Verifying iOS app zip file: ${finalAppFile}`);
|
|
277
277
|
}
|
|
278
278
|
await (0, methods_1.verifyAppZip)(finalAppFile);
|
|
279
279
|
}
|
|
@@ -337,12 +337,19 @@ class Cloud extends core_1.Command {
|
|
|
337
337
|
if (!finalAppFile)
|
|
338
338
|
throw new Error('You must provide either an app binary id or an app file');
|
|
339
339
|
if (debug) {
|
|
340
|
-
this.log(`DEBUG
|
|
340
|
+
this.log(`[DEBUG] Uploading binary file: ${finalAppFile}`);
|
|
341
341
|
}
|
|
342
|
-
const binaryId = await (0, methods_1.uploadBinary)(
|
|
342
|
+
const binaryId = await (0, methods_1.uploadBinary)({
|
|
343
|
+
apiKey,
|
|
344
|
+
apiUrl,
|
|
345
|
+
debug,
|
|
346
|
+
filePath: finalAppFile,
|
|
347
|
+
ignoreShaCheck,
|
|
348
|
+
log: !json,
|
|
349
|
+
});
|
|
343
350
|
finalBinaryId = binaryId;
|
|
344
351
|
if (debug) {
|
|
345
|
-
this.log(`DEBUG
|
|
352
|
+
this.log(`[DEBUG] Binary uploaded with ID: ${binaryId}`);
|
|
346
353
|
}
|
|
347
354
|
}
|
|
348
355
|
// finalBinaryId should always be defined after validation - fail fast if not
|
|
@@ -376,16 +383,15 @@ class Cloud extends core_1.Command {
|
|
|
376
383
|
retry,
|
|
377
384
|
runnerType,
|
|
378
385
|
showCrosshairs: flags['show-crosshairs'],
|
|
379
|
-
skipChromeOnboarding: flags['skip-chrome-onboarding'],
|
|
380
386
|
});
|
|
381
387
|
if (debug) {
|
|
382
|
-
this.log(`DEBUG
|
|
388
|
+
this.log(`[DEBUG] Submitting flow upload request to ${apiUrl}/uploads/flow`);
|
|
383
389
|
}
|
|
384
390
|
const { message, results } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl, apiKey, testFormData);
|
|
385
391
|
if (debug) {
|
|
386
|
-
this.log(`DEBUG
|
|
387
|
-
this.log(`DEBUG
|
|
388
|
-
this.log(`DEBUG
|
|
392
|
+
this.log(`[DEBUG] Flow upload response received`);
|
|
393
|
+
this.log(`[DEBUG] Message: ${message}`);
|
|
394
|
+
this.log(`[DEBUG] Results count: ${results?.length || 0}`);
|
|
389
395
|
}
|
|
390
396
|
if (!results?.length)
|
|
391
397
|
(0, errors_1.error)('No tests created: ' + message);
|
|
@@ -403,7 +409,7 @@ class Cloud extends core_1.Command {
|
|
|
403
409
|
this.log(styling_1.colors.dim('Poll upload status using: ') + styling_1.colors.info(`dcd status --api-key ... --upload-id ${results[0].test_upload_id}`));
|
|
404
410
|
if (async) {
|
|
405
411
|
if (debug) {
|
|
406
|
-
this.log(`DEBUG
|
|
412
|
+
this.log(`[DEBUG] Async flag is set, not waiting for results`);
|
|
407
413
|
}
|
|
408
414
|
const jsonOutput = {
|
|
409
415
|
consoleUrl: url,
|
|
@@ -517,8 +523,8 @@ class Cloud extends core_1.Command {
|
|
|
517
523
|
}
|
|
518
524
|
catch (error) {
|
|
519
525
|
if (debugFlag && error instanceof Error) {
|
|
520
|
-
this.log(`DEBUG
|
|
521
|
-
this.log(`DEBUG
|
|
526
|
+
this.log(`[DEBUG] Error in command execution: ${error.message}`);
|
|
527
|
+
this.log(`[DEBUG] Error stack: ${error.stack}`);
|
|
522
528
|
}
|
|
523
529
|
if (error instanceof Error && error.message === 'RUN_FAILED') {
|
|
524
530
|
if (jsonFile) {
|
package/dist/commands/upload.js
CHANGED
|
@@ -42,7 +42,14 @@ class Upload extends core_1.Command {
|
|
|
42
42
|
this.log((0, styling_1.sectionHeader)('Uploading app binary'));
|
|
43
43
|
this.log(` ${styling_1.colors.dim('→ File:')} ${styling_1.colors.highlight(appFile)}`);
|
|
44
44
|
this.log('');
|
|
45
|
-
const appBinaryId = await (0, methods_1.uploadBinary)(
|
|
45
|
+
const appBinaryId = await (0, methods_1.uploadBinary)({
|
|
46
|
+
apiKey,
|
|
47
|
+
apiUrl,
|
|
48
|
+
debug,
|
|
49
|
+
filePath: appFile,
|
|
50
|
+
ignoreShaCheck,
|
|
51
|
+
log: !json,
|
|
52
|
+
});
|
|
46
53
|
if (json) {
|
|
47
54
|
return { appBinaryId };
|
|
48
55
|
}
|
|
@@ -10,5 +10,4 @@ export declare const deviceFlags: {
|
|
|
10
10
|
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
11
|
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
12
|
'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
13
|
-
'skip-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
14
13
|
};
|
|
@@ -39,8 +39,4 @@ exports.deviceFlags = {
|
|
|
39
39
|
default: false,
|
|
40
40
|
description: '[Android only] Display crosshairs for screen interactions during test execution',
|
|
41
41
|
}),
|
|
42
|
-
'skip-chrome-onboarding': core_1.Flags.boolean({
|
|
43
|
-
default: false,
|
|
44
|
-
description: '[Android only] Skip Chrome browser onboarding screens when running tests',
|
|
45
|
-
}),
|
|
46
42
|
};
|
package/dist/constants.d.ts
CHANGED
|
@@ -42,7 +42,6 @@ export declare const flags: {
|
|
|
42
42
|
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
43
43
|
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
44
44
|
'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
45
|
-
'skip-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
46
45
|
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
47
46
|
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
48
47
|
'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
@@ -12,13 +12,24 @@ export declare const ApiGateway: {
|
|
|
12
12
|
exists: boolean;
|
|
13
13
|
}>;
|
|
14
14
|
downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
|
|
15
|
-
finaliseUpload(
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
finaliseUpload(config: {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
backblazeSuccess: boolean;
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
id: string;
|
|
20
|
+
metadata: TAppMetadata;
|
|
21
|
+
path: string;
|
|
22
|
+
sha: string;
|
|
23
|
+
supabaseSuccess: boolean;
|
|
24
|
+
}): Promise<Record<string, never>>;
|
|
25
|
+
getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios", fileSize: number): Promise<{
|
|
18
26
|
path: string;
|
|
19
|
-
|
|
27
|
+
tempPath: string;
|
|
28
|
+
finalPath: string;
|
|
20
29
|
id: string;
|
|
30
|
+
b2?: import("../types/generated/schema.types").components["schemas"]["B2UploadStrategy"];
|
|
21
31
|
}>;
|
|
32
|
+
finishLargeFile(baseUrl: string, apiKey: string, fileId: string, partSha1Array: string[]): Promise<any>;
|
|
22
33
|
getResultsForUpload(baseUrl: string, apiKey: string, uploadId: string): Promise<{
|
|
23
34
|
statusCode?: number;
|
|
24
35
|
results?: import("../types/generated/schema.types").components["schemas"]["TResultResponse"][];
|
|
@@ -29,6 +29,15 @@ exports.ApiGateway = {
|
|
|
29
29
|
throw new Error(`Authentication failed. Please check your API key.`);
|
|
30
30
|
}
|
|
31
31
|
case 403: {
|
|
32
|
+
// For 403, use the server's error message directly as it's now detailed
|
|
33
|
+
// If the message suggests an API key issue, provide additional guidance
|
|
34
|
+
if (userMessage.toLowerCase().includes('api key')) {
|
|
35
|
+
throw new Error(`${userMessage}\n\nTroubleshooting steps:\n` +
|
|
36
|
+
` 1. Verify DEVICE_CLOUD_API_KEY environment variable is set\n` +
|
|
37
|
+
` 2. Check you're using the correct API key for this environment\n` +
|
|
38
|
+
` 3. Ensure the API key hasn't been deleted or revoked\n` +
|
|
39
|
+
` 4. Confirm you're connecting to the correct API URL`);
|
|
40
|
+
}
|
|
32
41
|
throw new Error(`Access denied. ${userMessage}`);
|
|
33
42
|
}
|
|
34
43
|
case 404: {
|
|
@@ -98,10 +107,17 @@ exports.ApiGateway = {
|
|
|
98
107
|
const fileStream = createWriteStream(artifactsPath, { flags: 'w' });
|
|
99
108
|
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
100
109
|
},
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
async finaliseUpload(config) {
|
|
111
|
+
const { baseUrl, apiKey, id, metadata, path, sha, supabaseSuccess, backblazeSuccess } = config;
|
|
103
112
|
const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
|
|
104
|
-
body: JSON.stringify({
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
backblazeSuccess,
|
|
115
|
+
id,
|
|
116
|
+
metadata,
|
|
117
|
+
path, // This is tempPath for TUS uploads
|
|
118
|
+
sha,
|
|
119
|
+
supabaseSuccess,
|
|
120
|
+
}),
|
|
105
121
|
headers: {
|
|
106
122
|
'content-type': 'application/json',
|
|
107
123
|
'x-app-api-key': apiKey,
|
|
@@ -113,9 +129,9 @@ exports.ApiGateway = {
|
|
|
113
129
|
}
|
|
114
130
|
return res.json();
|
|
115
131
|
},
|
|
116
|
-
async getBinaryUploadUrl(baseUrl, apiKey, platform) {
|
|
132
|
+
async getBinaryUploadUrl(baseUrl, apiKey, platform, fileSize) {
|
|
117
133
|
const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
|
|
118
|
-
body: JSON.stringify({ platform }),
|
|
134
|
+
body: JSON.stringify({ platform, fileSize }),
|
|
119
135
|
headers: {
|
|
120
136
|
'content-type': 'application/json',
|
|
121
137
|
'x-app-api-key': apiKey,
|
|
@@ -127,6 +143,20 @@ exports.ApiGateway = {
|
|
|
127
143
|
}
|
|
128
144
|
return res.json();
|
|
129
145
|
},
|
|
146
|
+
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');
|
|
157
|
+
}
|
|
158
|
+
return res.json();
|
|
159
|
+
},
|
|
130
160
|
async getResultsForUpload(baseUrl, apiKey, uploadId) {
|
|
131
161
|
// TODO: merge with getUploadStatus
|
|
132
162
|
const res = await fetch(`${baseUrl}/results/${uploadId}`, {
|
|
@@ -7,5 +7,33 @@ export declare class SupabaseGateway {
|
|
|
7
7
|
SUPABASE_PUBLIC_KEY: string;
|
|
8
8
|
SUPABASE_URL: string;
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Upload to Supabase using resumable uploads (TUS protocol)
|
|
12
|
+
* Uploads to staging location (uploads/{id}/) using anon key
|
|
13
|
+
* File is later moved to final location by API after finalization
|
|
14
|
+
* @param env - Environment (dev or prod)
|
|
15
|
+
* @param path - Staging storage path (uploads/{id}/file.ext)
|
|
16
|
+
* @param file - File to upload
|
|
17
|
+
* @param debug - Enable debug logging
|
|
18
|
+
* @param onProgress - Optional callback for upload progress (bytesUploaded, bytesTotal)
|
|
19
|
+
* @returns Promise that resolves when upload completes
|
|
20
|
+
*/
|
|
21
|
+
static uploadResumable(env: 'dev' | 'prod', path: string, file: File, debug?: boolean, onProgress?: (bytesUploaded: number, bytesTotal: number) => void): Promise<void>;
|
|
10
22
|
static uploadToSignedUrl(env: 'dev' | 'prod', path: string, token: string, file: File, debug?: boolean): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Logs network error details for debugging
|
|
25
|
+
* @param error - Error object to analyze for network-related issues
|
|
26
|
+
* @returns void
|
|
27
|
+
*/
|
|
28
|
+
private static logNetworkError;
|
|
29
|
+
/**
|
|
30
|
+
* Logs upload exception details for debugging
|
|
31
|
+
* @param error - Exception that occurred during upload
|
|
32
|
+
* @param env - Environment (dev or prod)
|
|
33
|
+
* @param supabaseUrl - Supabase URL being used
|
|
34
|
+
* @param path - Upload path
|
|
35
|
+
* @param file - File being uploaded
|
|
36
|
+
* @returns void
|
|
37
|
+
*/
|
|
38
|
+
private static logUploadException;
|
|
11
39
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SupabaseGateway = void 0;
|
|
4
4
|
const supabase_js_1 = require("@supabase/supabase-js");
|
|
5
|
+
const tus = require("tus-js-client");
|
|
5
6
|
class SupabaseGateway {
|
|
6
7
|
static SB = {
|
|
7
8
|
dev: {
|
|
@@ -16,6 +17,108 @@ class SupabaseGateway {
|
|
|
16
17
|
static getSupabaseKeys(env) {
|
|
17
18
|
return this.SB[env];
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Upload to Supabase using resumable uploads (TUS protocol)
|
|
22
|
+
* Uploads to staging location (uploads/{id}/) using anon key
|
|
23
|
+
* File is later moved to final location by API after finalization
|
|
24
|
+
* @param env - Environment (dev or prod)
|
|
25
|
+
* @param path - Staging storage path (uploads/{id}/file.ext)
|
|
26
|
+
* @param file - File to upload
|
|
27
|
+
* @param debug - Enable debug logging
|
|
28
|
+
* @param onProgress - Optional callback for upload progress (bytesUploaded, bytesTotal)
|
|
29
|
+
* @returns Promise that resolves when upload completes
|
|
30
|
+
*/
|
|
31
|
+
static async uploadResumable(env, path, file, debug = false, onProgress) {
|
|
32
|
+
const { SUPABASE_PUBLIC_KEY, SUPABASE_URL } = this.getSupabaseKeys(env);
|
|
33
|
+
// Extract project ID from Supabase URL for the storage endpoint
|
|
34
|
+
// Format: https://project-id.supabase.co or https://cloud.devicecloud.dev (custom domain)
|
|
35
|
+
let projectId;
|
|
36
|
+
if (SUPABASE_URL.includes('.supabase.co')) {
|
|
37
|
+
projectId = SUPABASE_URL.replace('https://', '').split('.')[0];
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// For custom domains like cloud.devicecloud.dev, we need the project ref
|
|
41
|
+
// This is the dev environment project ref
|
|
42
|
+
projectId = env === 'dev' ? 'lbmsowehtjwnqlurpemb' : 'pgydnphbimetinsgfkbo';
|
|
43
|
+
}
|
|
44
|
+
const storageUrl = `https://${projectId}.storage.supabase.co`;
|
|
45
|
+
if (debug) {
|
|
46
|
+
console.log(`[DEBUG] Resumable upload starting...`);
|
|
47
|
+
console.log(`[DEBUG] Storage URL: ${storageUrl}`);
|
|
48
|
+
console.log(`[DEBUG] Upload path: ${path}`);
|
|
49
|
+
console.log(`[DEBUG] File name: ${file.name}`);
|
|
50
|
+
console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
|
|
51
|
+
}
|
|
52
|
+
// Convert File to Buffer for Node.js tus-js-client
|
|
53
|
+
// In Node.js environment, tus-js-client expects Buffer or Readable stream, not File
|
|
54
|
+
const fileBuffer = Buffer.from(await file.arrayBuffer());
|
|
55
|
+
if (debug) {
|
|
56
|
+
console.log(`[DEBUG] Converted File to Buffer (${fileBuffer.length} bytes)`);
|
|
57
|
+
}
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const upload = new tus.Upload(fileBuffer, {
|
|
60
|
+
// TUS endpoint for Supabase Storage
|
|
61
|
+
endpoint: `${storageUrl}/storage/v1/upload/resumable`,
|
|
62
|
+
// Retry configuration - will retry 5 times with increasing delays
|
|
63
|
+
retryDelays: [0, 3000, 5000, 10_000, 20_000],
|
|
64
|
+
// Authentication and headers
|
|
65
|
+
// Use anon key for staging uploads to uploads/* folder
|
|
66
|
+
headers: {
|
|
67
|
+
authorization: `Bearer ${SUPABASE_PUBLIC_KEY}`,
|
|
68
|
+
'x-upsert': 'true', // Allow overwriting existing files
|
|
69
|
+
},
|
|
70
|
+
// Upload metadata
|
|
71
|
+
metadata: {
|
|
72
|
+
bucketName: 'organizations',
|
|
73
|
+
objectName: path,
|
|
74
|
+
contentType: file.type || 'application/octet-stream',
|
|
75
|
+
cacheControl: '3600',
|
|
76
|
+
},
|
|
77
|
+
// Chunk size must be 6MB for Supabase
|
|
78
|
+
chunkSize: 6 * 1024 * 1024,
|
|
79
|
+
// Remove fingerprint on success to allow re-uploading identical files
|
|
80
|
+
removeFingerprintOnSuccess: true,
|
|
81
|
+
// Upload data during creation for better performance
|
|
82
|
+
uploadDataDuringCreation: true,
|
|
83
|
+
// Progress callback
|
|
84
|
+
onProgress(bytesUploaded, bytesTotal) {
|
|
85
|
+
if (debug) {
|
|
86
|
+
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
|
87
|
+
console.log(`[DEBUG] Upload progress: ${(bytesUploaded / 1024 / 1024).toFixed(2)} MB / ${(bytesTotal / 1024 / 1024).toFixed(2)} MB (${percentage}%)`);
|
|
88
|
+
}
|
|
89
|
+
// Call user-provided progress callback if available
|
|
90
|
+
if (onProgress) {
|
|
91
|
+
onProgress(bytesUploaded, bytesTotal);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
// Error handler
|
|
95
|
+
onError(error) {
|
|
96
|
+
if (debug) {
|
|
97
|
+
console.error(`[DEBUG] === RESUMABLE UPLOAD ERROR ===`);
|
|
98
|
+
console.error(`[DEBUG] Error:`, error);
|
|
99
|
+
console.error(`[DEBUG] Error message: ${error.message}`);
|
|
100
|
+
if (error.stack) {
|
|
101
|
+
console.error(`[DEBUG] Stack trace:\n${error.stack}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
reject(new Error(`Resumable upload failed: ${error.message}`));
|
|
105
|
+
},
|
|
106
|
+
// Success handler
|
|
107
|
+
onSuccess() {
|
|
108
|
+
if (debug) {
|
|
109
|
+
console.log(`[DEBUG] Resumable upload completed successfully`);
|
|
110
|
+
console.log(`[DEBUG] Upload path: ${path}`);
|
|
111
|
+
}
|
|
112
|
+
resolve();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
// Start the upload
|
|
116
|
+
if (debug) {
|
|
117
|
+
console.log(`[DEBUG] Starting TUS upload...`);
|
|
118
|
+
}
|
|
119
|
+
upload.start();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
19
122
|
static async uploadToSignedUrl(env, path, token, file, debug = false) {
|
|
20
123
|
const { SUPABASE_PUBLIC_KEY, SUPABASE_URL } = this.getSupabaseKeys(env);
|
|
21
124
|
if (debug) {
|
|
@@ -48,26 +151,63 @@ class SupabaseGateway {
|
|
|
48
151
|
}
|
|
49
152
|
catch (error) {
|
|
50
153
|
if (debug) {
|
|
51
|
-
|
|
52
|
-
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
53
|
-
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
-
// Check for common network errors
|
|
55
|
-
if (error instanceof Error) {
|
|
56
|
-
if (error.message.includes('ECONNREFUSED')) {
|
|
57
|
-
console.error(`[DEBUG] Network error: Connection refused - check internet connectivity`);
|
|
58
|
-
}
|
|
59
|
-
else if (error.message.includes('ETIMEDOUT')) {
|
|
60
|
-
console.error(`[DEBUG] Network error: Connection timeout - check internet connectivity or try again`);
|
|
61
|
-
}
|
|
62
|
-
else if (error.message.includes('ENOTFOUND')) {
|
|
63
|
-
console.error(`[DEBUG] Network error: DNS lookup failed - check internet connectivity`);
|
|
64
|
-
}
|
|
65
|
-
else if (error.message.includes('ECONNRESET')) {
|
|
66
|
-
console.error(`[DEBUG] Network error: Connection reset - network unstable or interrupted`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
154
|
+
this.logUploadException(error, env, SUPABASE_URL, path, file);
|
|
69
155
|
}
|
|
70
|
-
throw
|
|
156
|
+
// Re-throw with additional context
|
|
157
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
158
|
+
throw new Error(`Supabase upload error: ${errorMsg}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Logs network error details for debugging
|
|
163
|
+
* @param error - Error object to analyze for network-related issues
|
|
164
|
+
* @returns void
|
|
165
|
+
*/
|
|
166
|
+
static logNetworkError(error) {
|
|
167
|
+
const errorMsg = error.message.toLowerCase();
|
|
168
|
+
if (errorMsg.includes('econnrefused') || errorMsg.includes('connection refused')) {
|
|
169
|
+
console.error(`[DEBUG] Network error: Connection refused - check internet connectivity`);
|
|
170
|
+
}
|
|
171
|
+
else if (errorMsg.includes('etimedout') || errorMsg.includes('timeout')) {
|
|
172
|
+
console.error(`[DEBUG] Network error: Connection timeout - check internet connectivity or try again`);
|
|
173
|
+
}
|
|
174
|
+
else if (errorMsg.includes('enotfound') || errorMsg.includes('not found')) {
|
|
175
|
+
console.error(`[DEBUG] Network error: DNS lookup failed - check internet connectivity`);
|
|
176
|
+
}
|
|
177
|
+
else if (errorMsg.includes('econnreset') || errorMsg.includes('connection reset') || errorMsg.includes('connection lost')) {
|
|
178
|
+
console.error(`[DEBUG] Network error: Connection reset/lost - network unstable or interrupted`);
|
|
179
|
+
}
|
|
180
|
+
else if (errorMsg.includes('network')) {
|
|
181
|
+
console.error(`[DEBUG] Network-related error detected - check internet connectivity`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Logs upload exception details for debugging
|
|
186
|
+
* @param error - Exception that occurred during upload
|
|
187
|
+
* @param env - Environment (dev or prod)
|
|
188
|
+
* @param supabaseUrl - Supabase URL being used
|
|
189
|
+
* @param path - Upload path
|
|
190
|
+
* @param file - File being uploaded
|
|
191
|
+
* @returns void
|
|
192
|
+
*/
|
|
193
|
+
static logUploadException(error, env, supabaseUrl, path, file) {
|
|
194
|
+
console.error(`[DEBUG] === SUPABASE UPLOAD EXCEPTION ===`);
|
|
195
|
+
console.error(`[DEBUG] Exception caught:`, error);
|
|
196
|
+
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
197
|
+
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
198
|
+
if (error instanceof Error && error.stack) {
|
|
199
|
+
console.error(`[DEBUG] Error stack:\n${error.stack}`);
|
|
200
|
+
}
|
|
201
|
+
// Log additional context
|
|
202
|
+
console.error(`[DEBUG] Upload context:`);
|
|
203
|
+
console.error(`[DEBUG] - Environment: ${env}`);
|
|
204
|
+
console.error(`[DEBUG] - Supabase URL: ${supabaseUrl}`);
|
|
205
|
+
console.error(`[DEBUG] - Upload path: ${path}`);
|
|
206
|
+
console.error(`[DEBUG] - File name: ${file.name}`);
|
|
207
|
+
console.error(`[DEBUG] - File size: ${file.size} bytes`);
|
|
208
|
+
// Check for common network errors
|
|
209
|
+
if (error instanceof Error) {
|
|
210
|
+
this.logNetworkError(error);
|
|
71
211
|
}
|
|
72
212
|
}
|
|
73
213
|
}
|
package/dist/methods.d.ts
CHANGED
|
@@ -3,7 +3,15 @@ export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer<Ar
|
|
|
3
3
|
export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
|
|
4
4
|
export declare const compressFilesFromRelativePath: (basePath: string, files: string[], commonRoot: string) => Promise<Buffer<ArrayBuffer>>;
|
|
5
5
|
export declare const verifyAppZip: (zipPath: string) => Promise<void>;
|
|
6
|
-
|
|
6
|
+
interface UploadBinaryConfig {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
apiUrl: string;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
filePath: string;
|
|
11
|
+
ignoreShaCheck?: boolean;
|
|
12
|
+
log?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const uploadBinary: (config: UploadBinaryConfig) => Promise<string>;
|
|
7
15
|
/**
|
|
8
16
|
* Writes JSON data to a file with error handling
|
|
9
17
|
* @param filePath - Path to the output JSON file
|
|
@@ -21,3 +29,4 @@ export declare const writeJSONFile: (filePath: string, data: unknown, logger: {
|
|
|
21
29
|
* @returns Formatted duration string (e.g. "2m 30s" or "45s")
|
|
22
30
|
*/
|
|
23
31
|
export declare const formatDurationSeconds: (durationSeconds: number) => string;
|
|
32
|
+
export {};
|