Files
SkyArtShop/backend/utils/validation.js

246 lines
6.0 KiB
JavaScript
Raw Normal View History

2026-01-18 02:22:05 -06:00
/**
* 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,
};