feat: sync full workspace including web modules, docs, and configurations to Gitea

Optimized the root .gitignore to exclude virtual environments, node modules,
and temp folders to ensure clean and lightweight version tracking.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-06-09 18:12:25 +08:00
parent 351688006e
commit a27e3b8e43
1510 changed files with 162044 additions and 1517 deletions

View File

@@ -0,0 +1,439 @@
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<any> => {
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
});
}
}
});
}
};
}