mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:51:13 -05:00
208 lines
5.4 KiB
TypeScript
208 lines
5.4 KiB
TypeScript
/**
|
|
* Client-side validation for entity submissions
|
|
* Prevents missing required fields before database calls
|
|
*/
|
|
|
|
export interface ValidationResult {
|
|
valid: boolean;
|
|
missingFields: string[];
|
|
errorMessage?: string;
|
|
}
|
|
|
|
export interface SlugValidationResult extends ValidationResult {
|
|
suggestedSlug?: string;
|
|
}
|
|
|
|
/**
|
|
* Validates slug format matching database constraints
|
|
* Pattern: lowercase alphanumeric with hyphens only
|
|
* No consecutive hyphens, no leading/trailing hyphens
|
|
*/
|
|
export function validateSlugFormat(slug: string): SlugValidationResult {
|
|
if (!slug) {
|
|
return {
|
|
valid: false,
|
|
missingFields: ['slug'],
|
|
errorMessage: 'Slug is required'
|
|
};
|
|
}
|
|
|
|
// Must match DB regex: ^[a-z0-9]+(-[a-z0-9]+)*$
|
|
const slugRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
if (!slugRegex.test(slug)) {
|
|
const suggested = slug
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9-]/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
|
|
return {
|
|
valid: false,
|
|
missingFields: ['slug'],
|
|
errorMessage: 'Slug must be lowercase alphanumeric with hyphens only (no spaces or special characters)',
|
|
suggestedSlug: suggested
|
|
};
|
|
}
|
|
|
|
// Length constraints
|
|
if (slug.length < 2) {
|
|
return {
|
|
valid: false,
|
|
missingFields: ['slug'],
|
|
errorMessage: 'Slug too short (minimum 2 characters)'
|
|
};
|
|
}
|
|
if (slug.length > 100) {
|
|
return {
|
|
valid: false,
|
|
missingFields: ['slug'],
|
|
errorMessage: 'Slug too long (maximum 100 characters)'
|
|
};
|
|
}
|
|
|
|
// Reserved slugs that could conflict with routes
|
|
const reserved = [
|
|
'admin', 'api', 'auth', 'new', 'edit', 'delete', 'create',
|
|
'update', 'null', 'undefined', 'settings', 'profile', 'login',
|
|
'logout', 'signup', 'dashboard', 'moderator', 'moderation'
|
|
];
|
|
if (reserved.includes(slug)) {
|
|
return {
|
|
valid: false,
|
|
missingFields: ['slug'],
|
|
errorMessage: `'${slug}' is a reserved slug and cannot be used`,
|
|
suggestedSlug: `${slug}-1`
|
|
};
|
|
}
|
|
|
|
return { valid: true, missingFields: [] };
|
|
}
|
|
|
|
/**
|
|
* Validates required fields for park creation
|
|
*/
|
|
export function validateParkCreateFields(data: any): ValidationResult {
|
|
const missingFields: string[] = [];
|
|
|
|
if (!data.name?.trim()) missingFields.push('name');
|
|
if (!data.slug?.trim()) missingFields.push('slug');
|
|
if (!data.park_type) missingFields.push('park_type');
|
|
if (!data.status) missingFields.push('status');
|
|
|
|
if (missingFields.length > 0) {
|
|
return {
|
|
valid: false,
|
|
missingFields,
|
|
errorMessage: `Missing required fields for park creation: ${missingFields.join(', ')}`
|
|
};
|
|
}
|
|
|
|
// Validate slug format
|
|
if (data.slug?.trim()) {
|
|
const slugValidation = validateSlugFormat(data.slug.trim());
|
|
if (!slugValidation.valid) {
|
|
return slugValidation;
|
|
}
|
|
}
|
|
|
|
return { valid: true, missingFields: [] };
|
|
}
|
|
|
|
/**
|
|
* Validates required fields for ride creation
|
|
*/
|
|
export function validateRideCreateFields(data: any): ValidationResult {
|
|
const missingFields: string[] = [];
|
|
|
|
if (!data.name?.trim()) missingFields.push('name');
|
|
if (!data.slug?.trim()) missingFields.push('slug');
|
|
if (!data.category) missingFields.push('category');
|
|
if (!data.status) missingFields.push('status');
|
|
|
|
if (missingFields.length > 0) {
|
|
return {
|
|
valid: false,
|
|
missingFields,
|
|
errorMessage: `Missing required fields for ride creation: ${missingFields.join(', ')}`
|
|
};
|
|
}
|
|
|
|
// Validate slug format
|
|
if (data.slug?.trim()) {
|
|
const slugValidation = validateSlugFormat(data.slug.trim());
|
|
if (!slugValidation.valid) {
|
|
return slugValidation;
|
|
}
|
|
}
|
|
|
|
return { valid: true, missingFields: [] };
|
|
}
|
|
|
|
/**
|
|
* Validates required fields for company creation
|
|
*/
|
|
export function validateCompanyCreateFields(data: any): ValidationResult {
|
|
const missingFields: string[] = [];
|
|
|
|
if (!data.name?.trim()) missingFields.push('name');
|
|
if (!data.slug?.trim()) missingFields.push('slug');
|
|
if (!data.company_type) missingFields.push('company_type');
|
|
|
|
if (missingFields.length > 0) {
|
|
return {
|
|
valid: false,
|
|
missingFields,
|
|
errorMessage: `Missing required fields for company creation: ${missingFields.join(', ')}`
|
|
};
|
|
}
|
|
|
|
// Validate slug format
|
|
if (data.slug?.trim()) {
|
|
const slugValidation = validateSlugFormat(data.slug.trim());
|
|
if (!slugValidation.valid) {
|
|
return slugValidation;
|
|
}
|
|
}
|
|
|
|
return { valid: true, missingFields: [] };
|
|
}
|
|
|
|
/**
|
|
* Validates required fields for ride model creation
|
|
*/
|
|
export function validateRideModelCreateFields(data: any): ValidationResult {
|
|
const missingFields: string[] = [];
|
|
|
|
if (!data.name?.trim()) missingFields.push('name');
|
|
if (!data.slug?.trim()) missingFields.push('slug');
|
|
if (!data.manufacturer_id) missingFields.push('manufacturer_id');
|
|
if (!data.category) missingFields.push('category');
|
|
|
|
if (missingFields.length > 0) {
|
|
return {
|
|
valid: false,
|
|
missingFields,
|
|
errorMessage: `Missing required fields for ride model creation: ${missingFields.join(', ')}`
|
|
};
|
|
}
|
|
|
|
// Validate slug format
|
|
if (data.slug?.trim()) {
|
|
const slugValidation = validateSlugFormat(data.slug.trim());
|
|
if (!slugValidation.valid) {
|
|
return slugValidation;
|
|
}
|
|
}
|
|
|
|
return { valid: true, missingFields: [] };
|
|
}
|
|
|
|
/**
|
|
* Helper to throw validation error if validation fails
|
|
*/
|
|
export function assertValid(result: ValidationResult): void {
|
|
if (!result.valid) {
|
|
throw new Error(result.errorMessage);
|
|
}
|
|
}
|