/** * Input Validation Utilities * Reusable validation functions with consistent error messages */ const { AppError } = require("../middleware/errorHandler"); const { HTTP_STATUS } = require("../config/constants"); /** * Validate required fields * @param {Object} data - Data object to validate * @param {string[]} requiredFields - Array of required field names * @throws {AppError} If validation fails */ const validateRequiredFields = (data, requiredFields) => { const missingFields = requiredFields.filter( (field) => !data[field] || (typeof data[field] === "string" && data[field].trim() === ""), ); if (missingFields.length > 0) { throw new AppError( `Missing required fields: ${missingFields.join(", ")}`, HTTP_STATUS.BAD_REQUEST, ); } }; /** * Validate email format * @param {string} email - Email to validate * @returns {boolean} True if valid */ const isValidEmail = (email) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }; /** * Validate email field * @param {string} email - Email to validate * @throws {AppError} If validation fails */ const validateEmail = (email) => { if (!email || !isValidEmail(email)) { throw new AppError("Invalid email format", HTTP_STATUS.BAD_REQUEST); } }; /** * Validate UUID format * @param {string} id - UUID to validate * @returns {boolean} True if valid UUID */ const isValidUUID = (id) => { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; return uuidRegex.test(id); }; /** * Validate number range * @param {number} value - Value to validate * @param {number} min - Minimum value * @param {number} max - Maximum value * @param {string} fieldName - Field name for error message * @throws {AppError} If validation fails */ const validateNumberRange = (value, min, max, fieldName = "Value") => { const num = parseFloat(value); if (isNaN(num) || num < min || num > max) { throw new AppError( `${fieldName} must be between ${min} and ${max}`, HTTP_STATUS.BAD_REQUEST, ); } return num; }; /** * Validate string length * @param {string} value - String to validate * @param {number} min - Minimum length * @param {number} max - Maximum length * @param {string} fieldName - Field name for error message * @throws {AppError} If validation fails */ const validateStringLength = (value, min, max, fieldName = "Field") => { if (!value || value.length < min || value.length > max) { throw new AppError( `${fieldName} must be between ${min} and ${max} characters`, HTTP_STATUS.BAD_REQUEST, ); } }; /** * Sanitize string input (remove HTML tags, trim) * @param {string} input - String to sanitize * @returns {string} Sanitized string */ const sanitizeString = (input) => { if (typeof input !== "string") return ""; return input .replace(/<[^>]*>/g, "") // Remove HTML tags .trim(); }; /** * Validate and sanitize slug * @param {string} slug - Slug to validate * @returns {string} Valid slug * @throws {AppError} If validation fails */ const validateSlug = (slug) => { const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const sanitized = slug.toLowerCase().trim(); if (!slugRegex.test(sanitized)) { throw new AppError( "Slug can only contain lowercase letters, numbers, and hyphens", HTTP_STATUS.BAD_REQUEST, ); } return sanitized; }; /** * Generate slug from string * @param {string} text - Text to convert to slug * @returns {string} Generated slug */ const generateSlug = (text) => { return text .toLowerCase() .replace(/[^a-z0-9\s-]/g, "") .replace(/\s+/g, "-") .replace(/-+/g, "-") .trim(); }; /** * Validate pagination parameters * @param {Object} query - Query parameters * @returns {Object} Validated pagination params */ const validatePagination = (query) => { const page = Math.max(1, parseInt(query.page) || 1); const limit = Math.min(100, Math.max(1, parseInt(query.limit) || 20)); return { page, limit }; }; /** * Validate image file * @param {Object} file - Multer file object * @throws {AppError} If validation fails */ const validateImageFile = (file) => { const allowedMimeTypes = [ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/bmp", "image/tiff", "image/svg+xml", "image/x-icon", "image/vnd.microsoft.icon", "image/ico", "image/avif", "image/heic", "image/heif", ]; const maxSize = 10 * 1024 * 1024; // 10MB for larger image formats if (!file) { throw new AppError("No file provided", HTTP_STATUS.BAD_REQUEST); } if (!allowedMimeTypes.includes(file.mimetype)) { throw new AppError( "Invalid file type. Allowed: JPEG, PNG, GIF, WebP, BMP, TIFF, SVG, ICO, AVIF, HEIC", HTTP_STATUS.BAD_REQUEST, ); } if (file.size > maxSize) { throw new AppError( "File too large. Maximum size is 5MB", HTTP_STATUS.BAD_REQUEST, ); } }; /** * Validate price value * @param {number} price - Price to validate * @param {string} fieldName - Field name for error message * @returns {number} Validated price * @throws {AppError} If validation fails */ const validatePrice = (price, fieldName = "Price") => { return validateNumberRange(price, 0, 999999, fieldName); }; /** * Validate stock quantity * @param {number} stock - Stock to validate * @returns {number} Validated stock * @throws {AppError} If validation fails */ const validateStock = (stock) => { return validateNumberRange(stock, 0, 999999, "Stock quantity"); }; /** * Validate color code (hex format) * @param {string} colorCode - Color code to validate * @returns {boolean} True if valid */ const isValidColorCode = (colorCode) => { const hexRegex = /^#[0-9A-F]{6}$/i; return hexRegex.test(colorCode); }; module.exports = { validateRequiredFields, validateEmail, isValidEmail, isValidUUID, validateNumberRange, validateStringLength, sanitizeString, validateSlug, generateSlug, validatePagination, validateImageFile, validatePrice, validateStock, isValidColorCode, };