UNPKG

novel-reader-sdk

Version:

SDK for Novel Reader API

415 lines (352 loc) 10.2 kB
/** * Scraping Service SDK Client * * Type-safe client for consuming the scraping service API * Provides methods for all endpoints with proper error handling */ import type { // Core types NovelDetails, ChapterData, NovelSearchResult, SiteConfiguration, HealthStatus, DatabaseInfo, DatabaseId, // Health types HealthOverview, ProxyHealthStatus, BrowserHealthStatus, ServiceStatus, // Request types SearchNovelsRequest, ScrapeChapterRequest, BatchScrapeRequest, ValidateUrlRequest, TestSiteConfigRequest, LatestNovelsRequest, PopularNovelsRequest, NovelDetailsRequest, // Response types ScrapingServiceResponse, PaginatedResponse, } from './types'; import { fetchWithTimeout, parseJsonResponse, buildFullUrl, validateDatabaseId, logRequest, logResponse, withRetry, type RetryConfig, } from './utils'; import { ValidationError, } from './errors'; // ===== Client Configuration ===== export interface ScrapingServiceConfig { /** Base URL of the scraping service */ baseUrl: string; /** Request timeout in milliseconds */ timeout?: number; /** Retry configuration for failed requests */ retry?: Partial<RetryConfig>; /** Enable debug logging */ debug?: boolean; } // ===== Main Client Class ===== export class ScrapingServiceClient { private readonly config: Required<ScrapingServiceConfig>; constructor(config: ScrapingServiceConfig) { this.config = { timeout: 30000, retry: {}, debug: false, ...config, }; // Ensure base URL doesn't end with slash this.config.baseUrl = this.config.baseUrl.replace(/\/$/, ''); } // ===== Database Operations ===== /** * List available database scrapers */ async listDatabases(): Promise<DatabaseInfo[]> { return this.makeRequest<ScrapingServiceResponse<DatabaseInfo[]>>({ method: 'GET', path: '/api/v1/databases', }).then(response => response.results); } /** * Search novels in a database */ async searchNovels( databaseId: DatabaseId, request: SearchNovelsRequest ): Promise<PaginatedResponse<NovelSearchResult>> { validateDatabaseId(databaseId); return this.makeRequest<PaginatedResponse<NovelSearchResult>>({ method: 'GET', path: '/api/v1/databases/:id/search', params: { id: databaseId }, query: request, }); } /** * Get latest novels from a database */ async getLatestNovels( databaseId: DatabaseId, request: LatestNovelsRequest = {} ): Promise<PaginatedResponse<NovelSearchResult>> { validateDatabaseId(databaseId); return this.makeRequest<PaginatedResponse<NovelSearchResult>>({ method: 'GET', path: '/api/v1/databases/:id/latest', params: { id: databaseId }, query: request, }); } /** * Get popular novels from a database */ async getPopularNovels( databaseId: DatabaseId, request: PopularNovelsRequest = {} ): Promise<PaginatedResponse<NovelSearchResult>> { validateDatabaseId(databaseId); return this.makeRequest<PaginatedResponse<NovelSearchResult>>({ method: 'GET', path: '/api/v1/databases/:id/popular', params: { id: databaseId }, query: request, }); } /** * Get detailed novel information */ async getNovelDetails( databaseId: DatabaseId, request: NovelDetailsRequest ): Promise<NovelDetails> { validateDatabaseId(databaseId); if (!request.url) { throw new ValidationError('Novel URL is required'); } return this.makeRequest<ScrapingServiceResponse<NovelDetails>>({ method: 'GET', path: '/api/v1/databases/:id/novel', params: { id: databaseId }, query: request, }).then(response => response.results); } // ===== Chapter Operations ===== /** * Scrape single chapter content */ async scrapeChapter(request: ScrapeChapterRequest): Promise<ChapterData> { if (!request.url) { throw new ValidationError('Chapter URL is required'); } return this.makeRequest<ScrapingServiceResponse<ChapterData>>({ method: 'POST', path: '/api/v1/chapters/scrape', body: request, }).then(response => response.results); } /** * Scrape multiple chapters in batch */ async scrapeChaptersBatch(request: BatchScrapeRequest): Promise<ChapterData[]> { if (!request.urls || request.urls.length === 0) { throw new ValidationError('At least one URL is required'); } return this.makeRequest<ScrapingServiceResponse<ChapterData[]>>({ method: 'POST', path: '/api/v1/chapters/batch', body: request, }).then(response => response.results); } /** * Validate if URL is supported for scraping */ async validateChapterUrl(request: ValidateUrlRequest): Promise<{ supported: boolean; hostname: string }> { if (!request.url) { throw new ValidationError('URL is required'); } return this.makeRequest<ScrapingServiceResponse<{ supported: boolean; hostname: string }>>({ method: 'GET', path: '/api/v1/chapters/validate-url', query: request, }).then(response => response.results); } // ===== Site Configuration Operations ===== /** * List all site configurations */ async listSiteConfigurations(): Promise<SiteConfiguration[]> { return this.makeRequest<ScrapingServiceResponse<SiteConfiguration[]>>({ method: 'GET', path: '/api/v1/sites', }).then(response => response.results); } /** * Get site configuration by hostname */ async getSiteConfiguration(hostname: string): Promise<SiteConfiguration> { if (!hostname) { throw new ValidationError('Hostname is required'); } return this.makeRequest<ScrapingServiceResponse<SiteConfiguration>>({ method: 'GET', path: '/api/v1/sites/:hostname', params: { hostname }, }).then(response => response.results); } /** * Update site configuration */ async updateSiteConfiguration( hostname: string, update: Partial<SiteConfiguration> ): Promise<SiteConfiguration> { if (!hostname) { throw new ValidationError('Hostname is required'); } return this.makeRequest<ScrapingServiceResponse<SiteConfiguration>>({ method: 'PUT', path: '/api/v1/sites/:hostname', params: { hostname }, body: update, }).then(response => response.results); } /** * Test site configuration */ async testSiteConfiguration( hostname: string, request: TestSiteConfigRequest ): Promise<{ success: boolean; data?: ChapterData }> { if (!hostname) { throw new ValidationError('Hostname is required'); } if (!request.testUrl) { throw new ValidationError('Test URL is required'); } return this.makeRequest<ScrapingServiceResponse<{ success: boolean; data?: ChapterData }>>({ method: 'POST', path: '/api/v1/sites/:hostname/test', params: { hostname }, body: request, }).then(response => response.results); } // ===== Health Operations ===== /** * Get overall service health status * @deprecated Use getHealthOverview() for comprehensive health data */ async getHealthStatus(): Promise<HealthStatus> { return this.makeRequest<HealthStatus>({ method: 'GET', path: '/api/v1/health', }); } /** * Get comprehensive health overview for dashboards */ async getHealthOverview(): Promise<HealthOverview> { return this.makeRequest<HealthOverview>({ method: 'GET', path: '/api/v1/health/overview', }); } /** * Get detailed proxy health status */ async getProxyHealth(): Promise<ProxyHealthStatus> { return this.makeRequest<ProxyHealthStatus>({ method: 'GET', path: '/api/v1/health/proxy', }); } /** * Get browser manager health status */ async getBrowserHealth(): Promise<BrowserHealthStatus> { return this.makeRequest<BrowserHealthStatus>({ method: 'GET', path: '/api/v1/health/browser', }); } // ===== Private Request Handler ===== private async makeRequest<T>(options: { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: string; params?: Record<string, string>; query?: Record<string, unknown> | object; body?: unknown; }): Promise<T> { const { method, path, params, query, body } = options; // Build URL const url = buildFullUrl(this.config.baseUrl, path, params, query); // Prepare request options const fetchOptions: RequestInit & { timeout?: number } = { method, timeout: this.config.timeout, }; // Add body for POST/PUT requests if (body && (method === 'POST' || method === 'PUT')) { fetchOptions.body = JSON.stringify(body); } // Log request if debug enabled if (this.config.debug) { logRequest(method, url, body); } // Make request with retry logic return withRetry(async () => { const response = await fetchWithTimeout(url, fetchOptions); // Log response if debug enabled if (this.config.debug) { logResponse(url, response.status); } return parseJsonResponse<T>(response); }, this.config.retry); } } // ===== Convenience Factory Function ===== /** * Create a new scraping service client */ export function createScrapingServiceClient(config: ScrapingServiceConfig): ScrapingServiceClient { return new ScrapingServiceClient(config); } // ===== Type Exports for Main App ===== export type { // Core types NovelDetails, ChapterData, NovelSearchResult, SiteConfiguration, HealthStatus, DatabaseInfo, DatabaseId, // Health types HealthOverview, ProxyHealthStatus, BrowserHealthStatus, ServiceStatus, ProxyProviderBreakdown, // Request types SearchNovelsRequest, ScrapeChapterRequest, BatchScrapeRequest, ValidateUrlRequest, TestSiteConfigRequest, LatestNovelsRequest, PopularNovelsRequest, NovelDetailsRequest, // Response types ScrapingServiceResponse, PaginatedResponse, } from './types';