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>
225 lines
6.1 KiB
TypeScript
225 lines
6.1 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
});
|
|
}
|