import type { Plugin } from 'vite'; import { lowdbService } from './lowdbService'; import { ValidationError, validateRequired, validateFileName, validateRecordForInsert, validateRecordForUpdate } from './validation'; import Papa from 'papaparse'; import { buildAttachmentContentDisposition } from './utils/contentDisposition'; /** * Error response interface */ interface ErrorResponse { error: string; code: string; details?: any; timestamp: string; } /** * Custom error class for API errors */ class APIError extends Error { constructor( public statusCode: number, public code: string, message: string, public details?: any ) { super(message); this.name = 'APIError'; } } /** * Data Management API Plugin * Provides RESTful API endpoints for managing data tables */ export function dataManagementApiPlugin(): Plugin { return { name: 'data-management-api', async configureServer(server) { // Initialize the database await lowdbService.initialize(); // Helper function to parse JSON body const parseBody = (req: any): Promise => { return new Promise((resolve, reject) => { let body = ''; req.on('data', (chunk: any) => body += chunk); req.on('end', () => { try { resolve(body ? JSON.parse(body) : {}); } catch (e) { reject(new APIError(400, 'INVALID_JSON', 'Invalid JSON in request body')); } }); req.on('error', reject); }); }; // Helper function to send JSON response const sendJSON = (res: any, statusCode: number, data: any) => { res.statusCode = statusCode; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.end(JSON.stringify(data)); }; // Helper function to send error response const sendError = (res: any, statusCode: number, message: string, code?: string, details?: any) => { const errorResponse: ErrorResponse = { error: message, code: code || 'ERROR', timestamp: new Date().toISOString() }; if (details) { errorResponse.details = details; } sendJSON(res, statusCode, errorResponse); }; // Error logging function const logError = (error: Error | APIError, context: string) => { const timestamp = new Date().toISOString(); if (error instanceof APIError) { console.error(`[${timestamp}] [Data Management API] ${context}:`, { statusCode: error.statusCode, code: error.code, message: error.message, details: error.details }); } else { console.error(`[${timestamp}] [Data Management API] ${context}:`, { name: error.name, message: error.message, stack: error.stack }); } }; // Validation helper functions const validateRequiredField = (value: any, fieldName: string) => { try { validateRequired(value, fieldName); } catch (e: any) { throw new APIError(400, 'VALIDATION_ERROR', e.message); } }; const validateFileNameParam = (fileName: string) => { try { validateFileName(fileName); } catch (e: any) { throw new APIError(400, 'VALIDATION_ERROR', e.message); } }; const validateRecordDataForInsert = (data: any) => { try { validateRecordForInsert(data); } catch (e: any) { if (e instanceof ValidationError) { throw new APIError(400, 'VALIDATION_ERROR', e.message, e.details); } throw new APIError(400, 'VALIDATION_ERROR', e.message); } }; const validateRecordDataForUpdate = (data: any) => { try { validateRecordForUpdate(data); } catch (e: any) { if (e instanceof ValidationError) { throw new APIError(400, 'VALIDATION_ERROR', e.message, e.details); } throw new APIError(400, 'VALIDATION_ERROR', e.message); } }; // Main middleware for data management API server.middlewares.use(async (req: any, res: any, next: any) => { // Only handle /api/data/* routes if (!req.url.startsWith('/api/data')) { return next(); } try { // Parse URL const url = new URL(req.url, `http://${req.headers.host}`); const pathname = url.pathname; // Route: GET /api/data/tables - Get all tables if (pathname === '/api/data/tables' && req.method === 'GET') { const tables = await lowdbService.getAllTables(); sendJSON(res, 200, tables); return; } // Route: POST /api/data/tables - Create new table if (pathname === '/api/data/tables' && req.method === 'POST') { const body = await parseBody(req); const { tableName } = body; validateRequiredField(tableName, 'tableName'); // Generate fileName from tableName if not provided // Use tableName directly as fileName (supports Chinese) const fileName = tableName.trim(); validateFileNameParam(fileName); // Check if table already exists const exists = await lowdbService.tableExists(fileName); if (exists) { throw new APIError(400, 'TABLE_EXISTS', `Table '${tableName}' already exists`); } await lowdbService.createTable(fileName, tableName); sendJSON(res, 201, { success: true, fileName, tableName }); return; } // Route: PUT /api/data/tables/:fileName - Update table info (e.g., tableName) const updateTableMatch = pathname.match(/^\/api\/data\/tables\/([^/]+)$/); if (updateTableMatch && req.method === 'PUT') { const fileName = decodeURIComponent(updateTableMatch[1]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } const body = await parseBody(req); const { tableName } = body; if (tableName !== undefined) { await lowdbService.updateTableName(fileName, tableName); } sendJSON(res, 200, { success: true, fileName, tableName }); return; } // Route: DELETE /api/data/tables/:fileName - Delete table const deleteTableMatch = pathname.match(/^\/api\/data\/tables\/([^/]+)$/); if (deleteTableMatch && req.method === 'DELETE') { const fileName = decodeURIComponent(deleteTableMatch[1]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } await lowdbService.deleteTable(fileName); sendJSON(res, 200, { success: true }); return; } // Route: GET /api/data/:fileName/export - CSV Export (must be before :id route) const exportMatch = pathname.match(/^\/api\/data\/([^/]+)\/export$/); if (exportMatch && req.method === 'GET') { const fileName = decodeURIComponent(exportMatch[1]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } // Get all records const records = await lowdbService.getTable(fileName); // Convert to CSV const csv = Papa.unparse(records, { quotes: true, header: true }); // Send CSV response res.statusCode = 200; res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Disposition', buildAttachmentContentDisposition(`${fileName}.csv`)); res.end(csv); return; } // Route: GET /api/data/:fileName - Get all data from table const getTableMatch = pathname.match(/^\/api\/data\/([^/]+)$/); if (getTableMatch && req.method === 'GET') { const fileName = decodeURIComponent(getTableMatch[1]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } const records = await lowdbService.getTable(fileName); sendJSON(res, 200, records); return; } // Route: GET /api/data/:fileName/:id - Get single record const getRecordMatch = pathname.match(/^\/api\/data\/([^/]+)\/([^/]+)$/); if (getRecordMatch && req.method === 'GET') { const fileName = decodeURIComponent(getRecordMatch[1]); const id = decodeURIComponent(getRecordMatch[2]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } const record = await lowdbService.getRecord(fileName, id); if (!record) { throw new APIError(404, 'NOT_FOUND', `Record with id '${id}' not found`); } sendJSON(res, 200, record); return; } // Route: POST /api/data/:fileName - Insert new record const insertMatch = pathname.match(/^\/api\/data\/([^/]+)$/); if (insertMatch && req.method === 'POST') { const fileName = decodeURIComponent(insertMatch[1]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } const body = await parseBody(req); validateRecordDataForInsert(body); const newRecord = await lowdbService.insertData(fileName, body); sendJSON(res, 201, newRecord); return; } // Route: PUT /api/data/:fileName/:id - Update record const updateMatch = pathname.match(/^\/api\/data\/([^/]+)\/([^/]+)$/); if (updateMatch && req.method === 'PUT') { const fileName = decodeURIComponent(updateMatch[1]); const id = decodeURIComponent(updateMatch[2]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } const body = await parseBody(req); validateRecordDataForUpdate(body); try { const updatedRecord = await lowdbService.updateData(fileName, id, body); sendJSON(res, 200, updatedRecord); } catch (e: any) { if (e.message.includes('not found')) { throw new APIError(404, 'NOT_FOUND', `Record with id '${id}' not found`); } throw e; } return; } // Route: DELETE /api/data/:fileName/:id - Delete record const deleteMatch = pathname.match(/^\/api\/data\/([^/]+)\/([^/]+)$/); if (deleteMatch && req.method === 'DELETE') { const fileName = decodeURIComponent(deleteMatch[1]); const id = decodeURIComponent(deleteMatch[2]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } try { await lowdbService.deleteData(fileName, id); sendJSON(res, 200, { success: true }); } catch (e: any) { if (e.message.includes('not found')) { throw new APIError(404, 'NOT_FOUND', `Record with id '${id}' not found`); } throw e; } return; } // Route: POST /api/data/:fileName/import - CSV Import const importMatch = pathname.match(/^\/api\/data\/([^/]+)\/import$/); if (importMatch && req.method === 'POST') { const fileName = decodeURIComponent(importMatch[1]); validateFileNameParam(fileName); const exists = await lowdbService.tableExists(fileName); if (!exists) { throw new APIError(404, 'NOT_FOUND', `Table '${fileName}' not found`); } const body = await parseBody(req); const { csvData } = body; if (!csvData || typeof csvData !== 'string') { throw new APIError(400, 'VALIDATION_ERROR', 'Missing or invalid csvData parameter'); } // Parse CSV const parseResult = Papa.parse(csvData, { header: true, dynamicTyping: true, skipEmptyLines: true, transformHeader: (header) => header.trim() }); if (parseResult.errors.length > 0) { throw new APIError( 400, 'CSV_PARSE_ERROR', 'Failed to parse CSV file', { errors: parseResult.errors } ); } // Import data with upsert logic const importedRecords = await lowdbService.importCSV(fileName, parseResult.data); sendJSON(res, 200, { success: true, recordCount: importedRecords.length, records: importedRecords }); return; } // No route matched throw new APIError(404, 'NOT_FOUND', 'API endpoint not found'); } catch (e: any) { // Error handling middleware if (e instanceof APIError) { // Known API error logError(e, `${req.method} ${req.url}`); sendError(res, e.statusCode, e.message, e.code, e.details); } else if (e instanceof ValidationError) { // Validation error from lowdbService logError(e, `${req.method} ${req.url}`); sendError(res, 400, e.message, 'VALIDATION_ERROR', e.details); } else if (e.code === 'ENOENT') { // File not found error logError(e, `${req.method} ${req.url}`); sendError(res, 404, 'Resource not found', 'NOT_FOUND'); } else if (e.code === 'EACCES') { // Permission error logError(e, `${req.method} ${req.url}`); sendError(res, 500, 'Permission denied', 'PERMISSION_ERROR'); } else { // Unknown error logError(e, `${req.method} ${req.url}`); sendError(res, 500, 'Internal server error', 'SERVER_ERROR', { message: e.message }); } } }); } }; }