/** * TouchBistro API Client * Handles authentication, pagination, error handling, and rate limiting */ import type { TouchBistroConfig, PaginatedResponse, PaginationParams } from './types.js'; export class TouchBistroAPIError extends Error { constructor( message: string, public statusCode?: number, public responseBody?: unknown ) { super(message); this.name = 'TouchBistroAPIError'; } } export class TouchBistroAPIClient { private apiKey: string; private baseUrl: string; private venueId?: string; constructor(config: TouchBistroConfig) { this.apiKey = config.apiKey; this.baseUrl = config.baseUrl || 'https://api.touchbistro.com/v1'; this.venueId = config.venueId; } /** * Make an authenticated request to the TouchBistro API */ private async request( endpoint: string, options: RequestInit = {} ): Promise { const url = `${this.baseUrl}${endpoint}`; const headers: HeadersInit = { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json', ...options.headers, }; if (this.venueId) { headers['X-Venue-ID'] = this.venueId; } try { const response = await fetch(url, { ...options, headers, }); // Handle rate limiting if (response.status === 429) { const retryAfter = response.headers.get('Retry-After'); throw new TouchBistroAPIError( `Rate limited. Retry after ${retryAfter} seconds`, 429, { retryAfter } ); } // Parse response body const contentType = response.headers.get('content-type'); let data: unknown; if (contentType?.includes('application/json')) { data = await response.json(); } else { data = await response.text(); } // Handle errors if (!response.ok) { throw new TouchBistroAPIError( `TouchBistro API error: ${response.statusText}`, response.status, data ); } return data as T; } catch (error) { if (error instanceof TouchBistroAPIError) { throw error; } if (error instanceof Error) { throw new TouchBistroAPIError( `Network error: ${error.message}`, undefined, error ); } throw new TouchBistroAPIError('Unknown error occurred'); } } /** * GET request */ async get(endpoint: string, params?: Record): Promise { const queryString = params ? '?' + new URLSearchParams( Object.entries(params) .filter(([_, v]) => v !== undefined && v !== null) .map(([k, v]) => [k, String(v)]) ).toString() : ''; return this.request(`${endpoint}${queryString}`, { method: 'GET', }); } /** * POST request */ async post(endpoint: string, body?: unknown): Promise { return this.request(endpoint, { method: 'POST', body: body ? JSON.stringify(body) : undefined, }); } /** * PUT request */ async put(endpoint: string, body?: unknown): Promise { return this.request(endpoint, { method: 'PUT', body: body ? JSON.stringify(body) : undefined, }); } /** * PATCH request */ async patch(endpoint: string, body?: unknown): Promise { return this.request(endpoint, { method: 'PATCH', body: body ? JSON.stringify(body) : undefined, }); } /** * DELETE request */ async delete(endpoint: string): Promise { return this.request(endpoint, { method: 'DELETE', }); } /** * Paginated GET request - automatically handles pagination */ async getPaginated( endpoint: string, params: PaginationParams & Record = {} ): Promise> { const { page = 1, limit = 50, ...otherParams } = params; const response = await this.get<{ data: T[]; pagination?: { total?: number; page?: number; limit?: number; hasMore?: boolean; nextPage?: string; }; }>(endpoint, { page, limit, ...otherParams, }); // Normalize pagination response return { data: response.data || [], pagination: { total: response.pagination?.total || response.data?.length || 0, page: response.pagination?.page || page, limit: response.pagination?.limit || limit, hasMore: response.pagination?.hasMore || false, }, }; } /** * Fetch all pages automatically */ async getAllPages( endpoint: string, params: Record = {}, maxPages = 10 ): Promise { const allData: T[] = []; let page = 1; let hasMore = true; while (hasMore && page <= maxPages) { const response = await this.getPaginated(endpoint, { ...params, page, limit: 100, }); allData.push(...response.data); hasMore = response.pagination.hasMore; page++; } return allData; } /** * Health check */ async healthCheck(): Promise<{ status: string; timestamp: string }> { try { return await this.get('/health'); } catch (error) { return { status: 'error', timestamp: new Date().toISOString(), }; } } }