import axios, { AxiosInstance, AxiosError } from 'axios';
import { CoolifyConfig, Project, Application, Deployment, CoolifyError } from '../types';
import { logger, logApiCall, logApiError } from '../utils/logger';
export class CoolifyApiError extends Error {
constructor(
public code: string,
message: string,
public details?: any
) {
super(message);
this.name = 'CoolifyApiError';
}
}
export class CoolifyApiClient {
private client: AxiosInstance;
private maxRetries = 3;
private retryDelay = 1000;
constructor(private config: CoolifyConfig) {
this.client = axios.create({
baseURL: config.apiUrl,
headers: {
'Authorization': `Bearer ${config.apiToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
timeout: 30000,
});
this.setupInterceptors();
}
private setupInterceptors() {
this.client.interceptors.request.use(
(config) => {
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
config.metadata = { requestId };
logApiCall(config.method?.toUpperCase() || 'GET', config.url || '', requestId);
return config;
},
(error) => Promise.reject(error)
);
this.client.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const requestId = error.config?.metadata?.requestId;
if (this.shouldRetry(error) && (error.config?.metadata?.retryCount || 0) < this.maxRetries) {
error.config.metadata = { ...error.config.metadata, retryCount: (error.config?.metadata?.retryCount || 0) + 1 };
await this.delay(this.retryDelay * error.config.metadata.retryCount);
return this.client.request(error.config);
}
this.handleApiError(error, requestId);
return Promise.reject(error);
}
);
}
private shouldRetry(error: AxiosError): boolean {
if (!error.response) return true; // Network errors
const status = error.response.status;
return status >= 500 || status === 429; // Retry on server errors or rate limit
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private handleApiError(error: AxiosError, requestId?: string) {
logApiError(error, { requestId });
if (error.response) {
const { status, data } = error.response;
const coolifyError = data as CoolifyError;
switch (status) {
case 401:
throw new CoolifyApiError('UNAUTHORIZED', 'Invalid API token', coolifyError);
case 403:
throw new CoolifyApiError('FORBIDDEN', 'Insufficient permissions', coolifyError);
case 404:
throw new CoolifyApiError('NOT_FOUND', 'Resource not found', coolifyError);
case 409:
throw new CoolifyApiError('CONFLICT', 'Resource conflict', coolifyError);
case 422:
throw new CoolifyApiError('VALIDATION_ERROR', 'Invalid request data', coolifyError);
case 429:
throw new CoolifyApiError('RATE_LIMIT', 'Too many requests', coolifyError);
default:
throw new CoolifyApiError(
'API_ERROR',
coolifyError?.message || `HTTP ${status}`,
coolifyError
);
}
} else if (error.request) {
throw new CoolifyApiError('NETWORK_ERROR', 'Failed to connect to Coolify API');
} else {
throw new CoolifyApiError('UNKNOWN_ERROR', error.message);
}
}
// Projects
async listProjects(teamId?: string): Promise<Project[]> {
const params = teamId ? { teamId } : {};
const response = await this.client.get('/api/v1/projects', { params });
return response.data;
}
async createProject(data: { name: string; description?: string; teamId?: string }): Promise<Project> {
const response = await this.client.post('/api/v1/projects', {
...data,
teamId: data.teamId || this.config.defaultTeamId,
});
return response.data;
}
async getProject(id: string): Promise<Project> {
const response = await this.client.get(`/api/v1/projects/${id}`);
return response.data;
}
// Applications
async listApplications(projectId: string): Promise<Application[]> {
const response = await this.client.get(`/api/v1/projects/${projectId}/applications`);
return response.data;
}
async getApplication(id: string): Promise<Application> {
const response = await this.client.get(`/api/v1/applications/${id}`);
return response.data;
}
async createApplication(projectId: string, data: {
name: string;
description?: string;
type: 'dockerfile' | 'image' | 'compose';
gitRepository?: {
url: string;
branch?: string;
};
dockerImage?: string;
dockerCompose?: string;
environment?: Record<string, string>;
ports?: number[];
}): Promise<Application> {
const response = await this.client.post(`/api/v1/projects/${projectId}/applications`, {
...data,
environment: data.environment || {},
ports: data.ports || [],
});
return response.data;
}
async updateApplication(id: string, data: Partial<Application>): Promise<Application> {
const response = await this.client.patch(`/api/v1/applications/${id}`, data);
return response.data;
}
async deleteApplication(id: string): Promise<void> {
await this.client.delete(`/api/v1/applications/${id}`);
}
// Deployments
async deployApplication(id: string, force = false): Promise<Deployment> {
const response = await this.client.post(`/api/v1/applications/${id}/deploy`, { force });
return response.data;
}
async getDeploymentStatus(deploymentId: string): Promise<Deployment> {
const response = await this.client.get(`/api/v1/deployments/${deploymentId}`);
return response.data;
}
async getDeploymentLogs(deploymentId: string): Promise<string[]> {
const response = await this.client.get(`/api/v1/deployments/${deploymentId}/logs`);
return response.data;
}
async getLatestDeployment(applicationId: string): Promise<Deployment | null> {
try {
const response = await this.client.get(`/api/v1/applications/${applicationId}/deployments/latest`);
return response.data;
} catch (error: any) {
if (error.code === 'NOT_FOUND') return null;
throw error;
}
}
// Utility methods
async checkProjectQuota(projectId: string): Promise<{ currentApps: number; maxApps: number }> {
const apps = await this.listApplications(projectId);
const currentApps = apps.length;
const maxApps = this.config.maxAppsPerProject || 50;
return { currentApps, maxApps };
}
async checkNameAvailability(projectId: string, name: string): Promise<boolean> {
const apps = await this.listApplications(projectId);
return !apps.some(app => app.name === name);
}
}
// Augment axios config type to include metadata
declare module 'axios' {
interface AxiosRequestConfig {
metadata?: {
requestId?: string;
retryCount?: number;
};
}
}