/** * Service Base Class * Provides common patterns and utilities for API services * Reduces boilerplate and ensures consistency across services */ import { z, ZodSchema } from 'zod'; import { httpClient } from '@/lib/http-client'; import { validateData, validateDataStrict, validateArrayWithFallback, ValidationResult } from '@/lib/schema-validation'; import { QueryBuilder } from '@/lib/query-builder'; // Service configuration interface export interface ServiceConfig { basePath: string; enableLogging?: boolean; defaultTimeout?: number; retries?: number; } // Generic service response types export interface ServiceResponse { data: T; success: boolean; error?: string; } // Base service class export abstract class BaseService { protected config: Required; constructor(config: ServiceConfig) { this.config = { enableLogging: import.meta.env.DEV, defaultTimeout: 30000, retries: 3, ...config, }; } /** * Build full endpoint path */ protected buildEndpoint(path: string = ''): string { const base = this.config.basePath.replace(/\/$/, ''); // Remove trailing slash const cleanPath = path.replace(/^\//, ''); // Remove leading slash return cleanPath ? `${base}/${cleanPath}` : base; } /** * Execute HTTP GET request with validation */ protected async get( path: string, schema: ZodSchema, queryParams?: Record, options?: { context?: string; useArrayFallback?: boolean; timeout?: number; } ): Promise> { const { context, useArrayFallback, timeout } = options || {}; const endpoint = this.buildEndpoint(path); const url = queryParams ? QueryBuilder.create().params(queryParams).toUrl(endpoint) : endpoint; if (this.config.enableLogging) { console.log(`[Service:${this.constructor.name}] GET ${url}`); } try { const rawData = await httpClient.get(url, { timeout: timeout || this.config.defaultTimeout, retries: this.config.retries, }); if (useArrayFallback && Array.isArray(rawData)) { return validateArrayWithFallback(schema, rawData, { context }); } return validateData(schema, rawData, { context: context || `${this.constructor.name}.get`, logErrors: this.config.enableLogging, }); } catch (error) { const errorMessage = `GET ${endpoint} failed: ${error instanceof Error ? error.message : 'Unknown error'}`; if (this.config.enableLogging) { console.error(`[Service:${this.constructor.name}] ${errorMessage}`, error); } return { success: false, error: { message: errorMessage, details: [], }, }; } } /** * Execute HTTP POST request with validation */ protected async post( path: string, data: unknown, schema: ZodSchema, options?: { context?: string; timeout?: number; isFormData?: boolean; } ): Promise> { const { context, timeout, isFormData } = options || {}; const endpoint = this.buildEndpoint(path); if (this.config.enableLogging) { console.log(`[Service:${this.constructor.name}] POST ${endpoint}`, isFormData ? 'FormData' : data); } try { const rawData = isFormData ? await httpClient.post(endpoint, data as FormData, { timeout: timeout || this.config.defaultTimeout, retries: this.config.retries, }) : await httpClient.post(endpoint, data, { timeout: timeout || this.config.defaultTimeout, retries: this.config.retries, }); return validateData(schema, rawData, { context: context || `${this.constructor.name}.post`, logErrors: this.config.enableLogging, }); } catch (error) { const errorMessage = `POST ${endpoint} failed: ${error instanceof Error ? error.message : 'Unknown error'}`; if (this.config.enableLogging) { console.error(`[Service:${this.constructor.name}] ${errorMessage}`, error); } return { success: false, error: { message: errorMessage, details: [], }, }; } } /** * Execute HTTP PUT request with validation */ protected async put( path: string, data: unknown, schema: ZodSchema, options?: { context?: string; timeout?: number; } ): Promise> { const { context, timeout } = options || {}; const endpoint = this.buildEndpoint(path); if (this.config.enableLogging) { console.log(`[Service:${this.constructor.name}] PUT ${endpoint}`, data); } try { const rawData = await httpClient.put(endpoint, data, { timeout: timeout || this.config.defaultTimeout, retries: this.config.retries, }); return validateData(schema, rawData, { context: context || `${this.constructor.name}.put`, logErrors: this.config.enableLogging, }); } catch (error) { const errorMessage = `PUT ${endpoint} failed: ${error instanceof Error ? error.message : 'Unknown error'}`; if (this.config.enableLogging) { console.error(`[Service:${this.constructor.name}] ${errorMessage}`, error); } return { success: false, error: { message: errorMessage, details: [], }, }; } } /** * Execute HTTP DELETE request */ protected async delete( path: string, options?: { context?: string; timeout?: number; } ): Promise> { const { context, timeout } = options || {}; const endpoint = this.buildEndpoint(path); if (this.config.enableLogging) { console.log(`[Service:${this.constructor.name}] DELETE ${endpoint}`); } try { await httpClient.delete(endpoint, { timeout: timeout || this.config.defaultTimeout, retries: this.config.retries, }); return { success: true, data: undefined }; } catch (error) { const errorMessage = `DELETE ${endpoint} failed: ${error instanceof Error ? error.message : 'Unknown error'}`; if (this.config.enableLogging) { console.error(`[Service:${this.constructor.name}] ${errorMessage}`, error); } return { success: false, error: { message: errorMessage, details: [], }, }; } } /** * Strict validation wrapper (throws on failure) */ protected validateStrict(schema: ZodSchema, data: unknown, context?: string): T { return validateDataStrict(schema, data, context || this.constructor.name); } /** * Create a query builder for this service */ protected createQuery(): QueryBuilder { return QueryBuilder.create(); } /** * Log service events (only in development) */ protected log(message: string, ...args: unknown[]): void { if (this.config.enableLogging) { console.log(`[Service:${this.constructor.name}] ${message}`, ...args); } } /** * Log errors (only in development) */ protected logError(message: string, error?: unknown): void { if (this.config.enableLogging) { console.error(`[Service:${this.constructor.name}] ${message}`, error); } } } /** * CRUD Service mixin for common patterns */ export abstract class CrudService> extends BaseService { protected abstract entitySchema: ZodSchema; protected abstract createSchema: ZodSchema; protected abstract updateSchema: ZodSchema; /** * Get all entities */ async getAll(options?: { query?: Record; context?: string; useArrayFallback?: boolean; }): Promise> { return this.get('', this.entitySchema.array(), options?.query, { context: options?.context || 'getAll', useArrayFallback: options?.useArrayFallback ?? true, }); } /** * Get entity by ID */ async getById(id: string, options?: { context?: string }): Promise> { return this.get(id, this.entitySchema, undefined, { context: options?.context || `getById(${id})`, }); } /** * Create new entity */ async create(data: TCreate, options?: { context?: string }): Promise> { const validatedData = this.validateStrict(this.createSchema, data, 'create validation'); return this.post('', validatedData, this.entitySchema, { context: options?.context || 'create', }); } /** * Update entity */ async update(id: string, data: TUpdate, options?: { context?: string }): Promise> { const validatedData = this.validateStrict(this.updateSchema, data, 'update validation'); return this.put(id, validatedData, this.entitySchema, { context: options?.context || `update(${id})`, }); } /** * Delete entity */ async remove(id: string, options?: { context?: string }): Promise> { return this.delete(id, { context: options?.context || `delete(${id})`, }); } } /** * Factory function to create service instances */ export function createService( ServiceClass: new (config: ServiceConfig) => T, basePath: string, options?: Partial ): T { return new ServiceClass({ basePath, enableLogging: import.meta.env.DEV, ...options, }); }