import { API_BASE, apiUrl } from './api'; import type { Port, PortDetail, Category, Maintainer, Build, Stats, APIResponse, SearchResult, AutocompleteResult, PortInstallation, EnhancedStats, PopularPort } from './types'; export class MacPortsAPI { private baseUrl: string; constructor(baseUrl: string = API_BASE) { this.baseUrl = baseUrl.replace(/\/+$/, ''); } private async fetch(path: string, params?: Record): Promise { const url = new URL(`${this.baseUrl}${path}`); if (params) { Object.entries(params).forEach(([key, value]) => { if (Array.isArray(value)) { // For arrays, add multiple query params with the same name (OR logic) value.forEach(v => { if (v) url.searchParams.append(key, v); }); } else if (value) { url.searchParams.append(key, value); } }); } const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`API Error: ${response.status} ${response.statusText}`); } return response.json(); } // Ports async getPorts(params?: { search?: string; categories?: string; 'maintainers__github'?: string; limit?: number; page?: number; ordering?: string; }): Promise> { const searchParams: Record = {}; if (params?.search) searchParams.search = params.search; if (params?.categories) searchParams.categories = params.categories; if (params?.['maintainers__github']) searchParams['maintainers__github'] = params['maintainers__github']; if (params?.limit) searchParams.limit = params.limit.toString(); if (params?.page) searchParams.page = params.page.toString(); if (params?.ordering) searchParams.ordering = params.ordering; return this.fetch>('/ports/', searchParams); } async getPort(name: string): Promise { return this.fetch(`/ports/${encodeURIComponent(name)}/`); } // Categories async getCategories(): Promise> { return this.fetch>('/category/'); } async getCategory(name: string): Promise { return this.fetch(`/category/${encodeURIComponent(name)}/`); } // Maintainers async getMaintainers(): Promise> { return this.fetch>('/maintainer/'); } async getMaintainer(github: string): Promise { return this.fetch(`/maintainer/${encodeURIComponent(github)}/`); } // Build History async getBuilds(params?: { port_name?: string; builder_name?: string | string[]; status?: string | string[]; page?: number; page_size?: number; }): Promise> { const searchParams: Record = {}; if (params?.port_name) searchParams.port_name = params.port_name; if (params?.builder_name) { // Pass arrays directly for OR logic, single values as strings searchParams['builder_name__name'] = params.builder_name; } if (params?.status) { // Pass arrays directly for OR logic, single values as strings searchParams.status = params.status; } if (params?.page) searchParams.page = params.page.toString(); if (params?.page_size) searchParams.page_size = params.page_size.toString(); // Ensure proper ordering by time_start (newest first) searchParams.ordering = '-time_start'; return this.fetch>('/builds/', searchParams); } async getBuild(id: number): Promise { return this.fetch(`/builds/${id}/`); } // Autocomplete APIs async autocompleteCategories(query: string): Promise { const response = await this.fetch>('/autocomplete/category/', { q: query }); return response.results || []; } async autocompleteMaintainers(query: string): Promise { const response = await this.fetch>('/autocomplete/maintainer/', { q: query }); return response.results || []; } async autocompletePorts(query: string): Promise { const response = await this.fetch('/autocomplete/port/', { q: query, name: 'on' }); // Handle different response formats if (Array.isArray(response)) { return response.map(item => typeof item === 'string' ? { name: item } : { name: item.name || item.port || item.portname, description: item.description } ); } return response.results || response.ports || []; } async autocompleteCategory(query: string): Promise { const response = await this.fetch('/autocomplete/category/', { q: query }); return Array.isArray(response) ? response : response.results || []; } async autocompleteMaintainer(query: string): Promise { const response = await this.fetch('/autocomplete/maintainer/', { q: query }); return Array.isArray(response) ? response : response.results || []; } async autocompleteVariant(query: string): Promise { const response = await this.fetch('/autocomplete/variant/', { q: query }); return Array.isArray(response) ? response : response.results || []; } // Stats async getStats(): Promise { return this.fetch('/stats/'); } // Port-specific stats async getPortStats(name: string, params?: { days?: number; days_ago?: number; property?: string[]; sort_by?: string; }): Promise { const searchParams: Record = { name }; if (params?.days) searchParams.days = params.days.toString(); if (params?.days_ago) searchParams.days_ago = params.days_ago.toString(); if (params?.sort_by) searchParams.sort_by = params.sort_by; if (params?.property) searchParams.property = params.property; return this.fetch('/statistics/port', searchParams); } // Get port installation count async getPortInstallCount(name: string, days: number = 30): Promise<{requested: number, all: number}> { const response = await this.getPortStats(name, { days }); return response.result?.[0] || { requested: 0, all: 0 }; } // Get port monthly statistics async getPortMonthlyStats(name: string, includeVersions: boolean = false): Promise { const searchParams: Record = { name }; if (includeVersions) searchParams.include_versions = 'yes'; return this.fetch('/statistics/port/monthly', searchParams); } // Get port health information async getPortHealth(name: string): Promise { // Fetch HTML from Django view (not API endpoint) const baseUrl = this.baseUrl.replace('/api/v1', ''); const response = await fetch(`${baseUrl}/port/${name}/health/?port_name=${name}`); return response.text(); } // Get port trac tickets async getPortTickets(name: string): Promise { // Fetch HTML from Django view (not API endpoint) const baseUrl = this.baseUrl.replace('/api/v1', ''); const response = await fetch(`${baseUrl}/port/${name}/tickets/?port_name=${name}`); return response.text(); } async getBuildFiles(buildId: string): Promise { return this.fetch(`/files/${buildId}/`); } // Search API using Haystack async search(params: { name?: string; description?: string; categories?: string; maintainers?: string; files?: string; limit?: number; page?: number; }): Promise> { const searchParams: Record = {}; if (params.name) searchParams.name = params.name; if (params.description) searchParams.description = params.description; if (params.categories) searchParams.categories = params.categories; if (params.maintainers) searchParams.maintainers = params.maintainers; if (params.files) searchParams.files = params.files; if (params.limit) searchParams.limit = params.limit.toString(); if (params.page) searchParams.page = params.page.toString(); return this.fetch>('/search/', searchParams); } // Stats API methods async getEnhancedStats(): Promise { return this.fetch('/statistics/enhanced'); } async getGeneralStats(params?: { days?: number; days_ago?: number; property?: string[]; sort_by?: string; }): Promise { // Build URL manually to support multiple 'property' parameters const urlParams = new URLSearchParams(); if (params?.days) urlParams.append('days', params.days.toString()); if (params?.days_ago) urlParams.append('days_ago', params.days_ago.toString()); if (params?.sort_by) urlParams.append('sort_by', params.sort_by); if (params?.property) { params.property.forEach(prop => { urlParams.append('property', prop); }); } const url = apiUrl(`/statistics/?${urlParams.toString()}`); const response = await fetch(url); if (!response.ok) { throw new Error(`API request failed: ${response.statusText}`); } return response.json(); } async getPopularPorts(params?: { days?: number; limit?: number; }): Promise { const searchParams: Record = {}; if (params?.days) searchParams.days = params.days.toString(); if (params?.limit) searchParams.limit = params.limit.toString(); return this.fetch('/statistics/popular', searchParams); } } // Default API client instance export const api = new MacPortsAPI();