tdpw
Version:
CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support
332 lines • 12.1 kB
JavaScript
;
/**
* 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