/** * Validation service for data management * Handles validation of data records, IDs, and data types */ import { DataRecord } from './lowdbService'; /** * Custom validation error class */ export class ValidationError extends Error { constructor( message: string, public field?: string, public details?: any ) { super(message); this.name = 'ValidationError'; } } /** * Validate that a value is not empty */ export function validateRequired(value: any, fieldName: string): void { if (value === undefined || value === null || value === '') { throw new ValidationError(`Missing required field: ${fieldName}`, fieldName); } } /** * Validate file name format * File names can contain letters, numbers, underscores, hyphens, and Chinese characters */ export function validateFileName(fileName: string): void { if (!fileName || typeof fileName !== 'string') { throw new ValidationError('Invalid fileName parameter', 'fileName'); } // Trim whitespace const trimmed = fileName.trim(); if (trimmed === '') { throw new ValidationError('fileName cannot be empty', 'fileName'); } // Check for invalid characters in file name (allow Chinese characters) // Disallow: / \ : * ? " < > | if (/[/\\:*?"<>|]/.test(trimmed)) { throw new ValidationError( 'fileName cannot contain the following characters: / \\ : * ? " < > |', 'fileName' ); } } /** * Validate that ID field exists and is a valid string or number * Requirement 3.2: Validate the required id field before persisting * Updated to support both string and number IDs */ export function validateIdField(id: any): void { if (id === undefined || id === null) { throw new ValidationError('Missing required field: id', 'id'); } // Accept both string and number types if (typeof id !== 'string' && typeof id !== 'number') { throw new ValidationError('Invalid id: must be a string or number', 'id'); } // For string IDs, check if empty if (typeof id === 'string' && id.trim() === '') { throw new ValidationError('Invalid id: cannot be empty', 'id'); } // For number IDs, check if valid (not NaN, not Infinity) if (typeof id === 'number' && (!Number.isFinite(id))) { throw new ValidationError('Invalid id: must be a finite number', 'id'); } } /** * Validate data types for record fields * Requirement 3.4: Validate data types for all fields */ export function validateDataTypes(data: any): void { if (!data || typeof data !== 'object') { throw new ValidationError('Invalid data: must be an object'); } // Check if data is an array (not allowed for single record) if (Array.isArray(data)) { throw new ValidationError('Invalid data: expected object, got array'); } // Validate that all values are of acceptable types for (const [key, value] of Object.entries(data)) { // Skip id field as it's validated separately if (key === 'id') continue; // Check for undefined values (null is allowed) if (value === undefined) { throw new ValidationError( `Invalid data type for field '${key}': undefined is not allowed`, key ); } // Check for functions (not allowed in JSON) if (typeof value === 'function') { throw new ValidationError( `Invalid data type for field '${key}': functions are not allowed`, key ); } // Check for symbols (not allowed in JSON) if (typeof value === 'symbol') { throw new ValidationError( `Invalid data type for field '${key}': symbols are not allowed`, key ); } // Nested objects are allowed, but check for circular references if (typeof value === 'object' && value !== null) { try { JSON.stringify(value); } catch (e) { throw new ValidationError( `Invalid data type for field '${key}': circular reference or non-serializable value`, key ); } } } } /** * Check for duplicate ID in existing records * Requirement 3.3: Detect duplicate IDs * Updated to support both string and number IDs using loose equality */ export function checkDuplicateId(id: string | number, existingRecords: DataRecord[], excludeId?: string | number): void { const duplicate = existingRecords.find(record => { // If excludeId is provided (for updates), skip that record if (excludeId !== undefined && record.id == excludeId) { return false; } // Use loose equality to match both string and number IDs return record.id == id; }); if (duplicate) { throw new ValidationError( `Duplicate id: a record with id '${id}' already exists`, 'id', { duplicateId: id } ); } } /** * Validate a complete record for insertion * Combines all validation checks */ export function validateRecordForInsert(data: any): void { // Validate data structure validateDataTypes(data); // If id is provided, validate it if (data.id !== undefined) { validateIdField(data.id); } } /** * Validate a record for update * Similar to insert but allows partial data */ export function validateRecordForUpdate(data: any): void { // Validate data structure validateDataTypes(data); // If id is provided in update data, validate it if (data.id !== undefined) { validateIdField(data.id); } } /** * Validate CSV import data * Checks each row for valid structure */ export function validateCSVData(csvData: any[]): void { if (!Array.isArray(csvData)) { throw new ValidationError('Invalid CSV data: must be an array'); } if (csvData.length === 0) { throw new ValidationError('Invalid CSV data: cannot be empty'); } // Validate each row csvData.forEach((row, index) => { try { validateDataTypes(row); // If row has id, validate it (support both string and number) if (row.id !== undefined && row.id !== null && row.id !== '') { validateIdField(row.id); } } catch (e) { if (e instanceof ValidationError) { throw new ValidationError( `Invalid data in CSV row ${index + 1}: ${e.message}`, e.field, { row: index + 1, ...e.details } ); } throw e; } }); }