UNPKG

tdpw

Version:

CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support

332 lines 12.1 kB
"use strict"; /** * Cache API client extension for TestDino API */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CacheApiClient = void 0; const types_1 = require("../types"); const retry_1 = require("../utils/retry"); const version_1 = require("../version"); /** * Cache API client for submitting test metadata to TestDino */ class CacheApiClient { baseUrl; apiKey; constructor(config) { this.baseUrl = config.apiUrl; this.apiKey = config.token; } /** * Submit cache data to TestDino API with retry logic */ async submitCacheData(payload) { return await (0, retry_1.withRetry)(async () => this.submitCacheDataAttempt(payload), { maxAttempts: 3, baseDelay: 1000, maxDelay: 5000, }); } /** * Single attempt to submit cache data */ async submitCacheDataAttempt(payload) { const url = `${this.baseUrl}/api/cache`; try { const response = await fetch(url, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(payload), }); if (!response.ok) { await this.handleHttpError(response); } const data = await response.json(); return this.parseResponse(data); } catch (error) { if (error instanceof types_1.AuthenticationError || error instanceof types_1.NetworkError) { throw error; } // Handle network-level errors const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new types_1.NetworkError(`Failed to submit cache data: ${errorMessage}`); } } /** * Headers for API requests */ getHeaders() { return { 'Content-Type': 'application/json', 'User-Agent': `tdpw-cache/${version_1.VERSION}`, 'X-API-Key': this.apiKey, }; } /** * Handle HTTP error responses */ async handleHttpError(response) { let errorBody; try { errorBody = await response.text(); } catch { errorBody = 'Unable to read error response'; } switch (response.status) { case 401: throw new types_1.AuthenticationError('Invalid API key for cache submission'); case 403: throw new types_1.AuthenticationError('API key does not have permission to submit cache data'); case 400: throw new types_1.NetworkError(`Bad cache data format: ${errorBody}`); case 404: throw new types_1.NetworkError('Cache endpoint not found - feature may not be available yet'); case 409: { // Conflict - cache data already exists (not an error, don't retry) // This is a non-retryable error - mark it specially so retry logic stops const conflictError = new types_1.NetworkError('Cache data already exists for this shard'); conflictError.retryable = false; throw conflictError; } case 413: throw new types_1.NetworkError('Cache payload too large - consider reducing failure data'); case 429: throw new types_1.NetworkError('Rate limit exceeded for cache submissions'); case 500: case 502: case 503: case 504: throw new types_1.NetworkError(`Server error submitting cache data (${response.status}) - will retry`); default: throw new types_1.NetworkError(`Cache submission failed: HTTP ${response.status} - ${errorBody}`); } } /** * Parse and validate API response */ parseResponse(data) { if (!data || typeof data !== 'object') { throw new types_1.NetworkError('Invalid cache submission response format'); } const response = data; // Handle response with nested data structure if ('success' in response && 'data' in response && typeof response.data === 'object') { const responseData = response.data; return { success: Boolean(response.success), cacheId: String(responseData.cacheId || 'unknown'), message: response.message ? String(response.message) : undefined, }; } // Handle flat response format if ('success' in response && 'cacheId' in response) { return { success: Boolean(response.success), cacheId: String(response.cacheId || 'unknown'), message: response.message ? String(response.message) : undefined, }; } // Fallback: assume success if we got a response return { success: true, cacheId: response.cacheId ? String(response.cacheId) : 'unknown', message: response.message ? String(response.message) : undefined, }; } /** * Test if cache API endpoint is available (health check) */ async testCacheEndpoint() { try { const response = await fetch(`${this.baseUrl}/api/v1/cache`, { method: 'OPTIONS', // Preflight request headers: { 'User-Agent': `tdpw-cache/${version_1.VERSION}`, }, }); // Even 404 means the server is responding return response.status < 500; } catch { return false; } } /** * Mock cache submission for development/testing */ async mockCacheSubmission(payload) { // Simulate API call delay await new Promise(resolve => setTimeout(resolve, 500)); console.log('🧪 MOCK: Would submit cache data to /api/v1/cache:', { cacheId: payload.cacheId, pipelineId: payload.pipelineId, commit: payload.commit, isSharded: payload.isSharded, shardIndex: payload.shardIndex, shardTotal: payload.shardTotal, failures: payload.failures.length, totalTests: payload.summary.total, }); return { success: true, cacheId: payload.cacheId, message: payload.isSharded ? `Mock cache submission successful for shard ${payload.shardIndex}/${payload.shardTotal}` : 'Mock cache submission successful', }; } /** * Retrieve cache data for a specific cache ID */ async retrieveCache(options) { // Validate mutually exclusive parameters if (options.shardIndex !== undefined && options.forAllShards) { throw new Error('Cannot specify both shardIndex and forAllShards parameters'); } const url = new URL(`${this.baseUrl}/api/cache/${options.cacheId}`); // Add query parameters if (options.shardIndex !== undefined) { url.searchParams.append('shardIndex', options.shardIndex.toString()); } if (options.forAllShards === true) { url.searchParams.append('forAllShards', 'true'); } try { const response = await fetch(url.toString(), { method: 'GET', headers: this.getHeaders(), }); if (!response.ok) { return this.handleRetrievalError(response, options.cacheId); } const data = (await response.json()); return data; } catch (error) { // Network or parsing error return { success: false, message: `Failed to retrieve cache: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * Handle retrieval errors with appropriate messages */ async handleRetrievalError(response, cacheId) { let errorMessage = 'Failed to retrieve cache data'; let errorBody; try { errorBody = (await response.json()); errorMessage = errorBody.message || errorMessage; } catch { // If response body is not JSON, use status text errorMessage = response.statusText || errorMessage; } switch (response.status) { case 400: return { success: false, message: errorMessage || 'Invalid request: Cannot specify both shardIndex and forAllShards', }; case 401: case 403: return { success: false, message: 'Unauthorized: Invalid or missing API token', }; case 404: return { success: false, message: `Cache not found for cache ID: ${cacheId}`, }; default: return { success: false, message: `${errorMessage} (HTTP ${response.status})`, }; } } /** * Get cache data for specific cache ID (simplified for last-failed command) */ async getCacheData(cacheId, options) { try { const url = new URL(`${this.baseUrl}/api/cache/${cacheId}`); // Add query parameters if provided if (options?.branch) { url.searchParams.append('branch', options.branch); } if (options?.commit) { url.searchParams.append('commit', options.commit); } const response = await fetch(url.toString(), { method: 'GET', headers: this.getHeaders(), }); if (!response.ok) { // Return null for 404 or other errors - let caller handle fallback return null; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = (await response.json()); // Extract the data we need for last-failed command if (data?.data?.cache) { return { cacheId: data.data.cacheId || cacheId, failures: data.data.cache.failures || [], branch: data.data.cache.branch || 'unknown', repository: data.data.cache.repository || 'unknown', }; } return null; } catch (_error) { // Network error - return null to allow fallback return null; } } /** * Get latest cache data (with optional branch filter) */ async getLatestCacheData(branch) { try { const url = new URL(`${this.baseUrl}/api/cache/latest`); if (branch) { url.searchParams.append('branch', branch); } const response = await fetch(url.toString(), { method: 'GET', headers: this.getHeaders(), }); if (!response.ok) { return null; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = (await response.json()); // Extract the data we need for last-failed command if (data?.data?.cache) { return { cacheId: data.data.cacheId || 'unknown', failures: data.data.cache.failures || [], branch: data.data.cache.branch || 'unknown', repository: data.data.cache.repository || 'unknown', }; } return null; } catch (_error) { return null; } } } exports.CacheApiClient = CacheApiClient; //# sourceMappingURL=cache-api.js.map