/** * API Helper Utilities * Common utilities for API endpoints including query parameter parsing */ import { NextRequest } from 'next/server'; export interface QueryOptions { page?: number; limit?: number; offset?: number; includeDeleted?: boolean; sort?: string; order?: 'ASC' | 'DESC'; filters?: Record; } export interface PaginationInfo { page: number; limit: number; offset: number; total: number; totalPages: number; hasMore: boolean; hasPrevious: boolean; } /** * Parse query parameters from Next.js request * @param request Next.js request object * @param defaults Default values for query options * @returns Parsed query options */ export function parseQueryParams( request: NextRequest, defaults: Partial = {} ): QueryOptions { const searchParams = request.nextUrl.searchParams; // Parse pagination const page = parseInt(searchParams.get('page') || String(defaults.page || 1)); const limit = parseInt(searchParams.get('limit') || String(defaults.limit || 100)); const offset = (page - 1) * limit; // Parse includeDeleted flag const includeDeletedParam = searchParams.get('includeDeleted'); const includeDeleted = includeDeletedParam !== null ? includeDeletedParam === 'true' : defaults.includeDeleted || false; // Parse sorting const sort = searchParams.get('sort') || defaults.sort || 'id'; const orderParam = searchParams.get('order')?.toUpperCase(); const order = (orderParam === 'ASC' || orderParam === 'DESC') ? orderParam : (defaults.order || 'ASC'); // Parse filters const filters: Record = { ...defaults.filters }; // Get all search params and treat unknown params as filters searchParams.forEach((value, key) => { // Skip known pagination/sorting params if (['page', 'limit', 'includeDeleted', 'sort', 'order'].includes(key)) { return; } // Parse filter value filters[key] = parseFilterValue(value); }); return { page, limit, offset, includeDeleted, sort, order, filters, }; } /** * Parse filter value to appropriate type * @param value String value from query parameter * @returns Parsed value (boolean, number, or string) */ function parseFilterValue(value: string): any { // Boolean if (value === 'true') return true; if (value === 'false') return false; // Number if (/^\d+$/.test(value)) return parseInt(value); if (/^\d+\.\d+$/.test(value)) return parseFloat(value); // Null if (value === 'null') return null; // String (default) return value; } /** * Build WHERE clause from filters * @param filters Filter object * @param includeDeleted Whether to include deleted records * @returns WHERE clause object */ export function buildWhereClause( filters: Record, includeDeleted: boolean = false ): Record { const where: Record = { ...filters }; // Always exclude soft-deleted records unless explicitly requested if (!includeDeleted) { where.is_deleted = false; } return where; } /** * Build ORDER BY clause * @param sort Sort field * @param order Sort order (ASC/DESC) * @returns ORDER BY string */ export function buildOrderByClause(sort: string, order: 'ASC' | 'DESC'): string { // Sanitize sort field to prevent SQL injection const sanitizedSort = sort.replace(/[^a-zA-Z0-9_]/g, ''); return `${sanitizedSort} ${order}`; } /** * Create pagination info object * @param page Current page number * @param limit Records per page * @param total Total record count * @returns Pagination information */ export function createPaginationInfo( page: number, limit: number, total: number ): PaginationInfo { const offset = (page - 1) * limit; const totalPages = Math.ceil(total / limit); return { page, limit, offset, total, totalPages, hasMore: page < totalPages, hasPrevious: page > 1, }; } /** * Validate query parameters * @param options Query options to validate * @throws Error if validation fails */ export function validateQueryParams(options: QueryOptions): void { if (options.page && options.page < 1) { throw new Error('Page must be greater than 0'); } if (options.limit && (options.limit < 1 || options.limit > 1000)) { throw new Error('Limit must be between 1 and 1000'); } if (options.sort && !/^[a-zA-Z0-9_]+$/.test(options.sort)) { throw new Error('Invalid sort field'); } } /** * Format API response with data and pagination * @param data Data array * @param pagination Pagination info * @param meta Additional metadata * @returns Formatted response object */ export function formatApiResponse( data: T[], pagination: PaginationInfo, meta?: Record ) { return { data, pagination, meta: { timestamp: new Date().toISOString(), ...meta, }, }; } /** * Handle API errors consistently * @param error Error object * @param context Error context * @returns Error response object */ export function handleApiError(error: any, context?: string) { console.error(`API Error${context ? ` (${context})` : ''}:`, error); const message = error instanceof Error ? error.message : 'An unexpected error occurred'; const statusCode = error.statusCode || 500; return { error: message, context, timestamp: new Date().toISOString(), statusCode, }; }