New projects created: - michangarrito (marketplace mobile) - template-saas (SaaS template) - clinica-dental (dental ERP) - clinica-veterinaria (veterinary ERP) Architecture updates: - Move catalog from core/ to shared/ - Add MCP servers structure and templates - Add git management scripts - Update SUBREPOSITORIOS.md with 15 new repos - Update .gitignore for new projects Repository infrastructure: - 4 main repositories - 11 subrepositorios - Gitea remotes configured 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
329 lines
7.4 KiB
TypeScript
329 lines
7.4 KiB
TypeScript
/**
|
|
* Validation Utilities - Core Module
|
|
*
|
|
* Framework-agnostic validation helper functions.
|
|
* Can be used in any project (NestJS, Express, Frontend, etc.)
|
|
*
|
|
* @module @shared/utils/validation
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
/**
|
|
* Check if value is email
|
|
*/
|
|
export const isEmail = (email: string): boolean => {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
};
|
|
|
|
/**
|
|
* Check if value is UUID (v4)
|
|
*/
|
|
export const isUUID = (uuid: string): boolean => {
|
|
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(uuid);
|
|
};
|
|
|
|
/**
|
|
* Check if value is valid URL
|
|
*/
|
|
export const isURL = (url: string): boolean => {
|
|
try {
|
|
new URL(url);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if password is strong
|
|
* Requirements: 8+ chars, 1 uppercase, 1 lowercase, 1 number, 1 special char
|
|
*/
|
|
export const isStrongPassword = (password: string): boolean => {
|
|
const strongRegex =
|
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
|
|
return strongRegex.test(password);
|
|
};
|
|
|
|
/**
|
|
* Get password strength (0-4)
|
|
* 0: Very weak, 1: Weak, 2: Fair, 3: Strong, 4: Very strong
|
|
*/
|
|
export const getPasswordStrength = (password: string): number => {
|
|
let strength = 0;
|
|
|
|
if (password.length >= 8) strength++;
|
|
if (password.length >= 12) strength++;
|
|
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
|
if (/\d/.test(password)) strength++;
|
|
if (/[@$!%*?&#^()_+=[\]{};':"\\|,.<>/?-]/.test(password)) strength++;
|
|
|
|
return Math.min(strength, 4);
|
|
};
|
|
|
|
/**
|
|
* Check if value is phone number (international format)
|
|
*/
|
|
export const isPhoneNumber = (phone: string): boolean => {
|
|
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
|
|
return phoneRegex.test(phone.replace(/[\s\-()]/g, ''));
|
|
};
|
|
|
|
/**
|
|
* Check if value is numeric
|
|
*/
|
|
export const isNumeric = (value: string): boolean => {
|
|
return !isNaN(Number(value)) && !isNaN(parseFloat(value));
|
|
};
|
|
|
|
/**
|
|
* Check if value is integer
|
|
*/
|
|
export const isInteger = (value: string | number): boolean => {
|
|
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
return Number.isInteger(num);
|
|
};
|
|
|
|
/**
|
|
* Check if value is alphanumeric
|
|
*/
|
|
export const isAlphanumeric = (value: string): boolean => {
|
|
const alphanumericRegex = /^[a-zA-Z0-9]+$/;
|
|
return alphanumericRegex.test(value);
|
|
};
|
|
|
|
/**
|
|
* Check if value is alphabetic only
|
|
*/
|
|
export const isAlphabetic = (value: string): boolean => {
|
|
const alphabeticRegex = /^[a-zA-Z]+$/;
|
|
return alphabeticRegex.test(value);
|
|
};
|
|
|
|
/**
|
|
* Check if value is within range (inclusive)
|
|
*/
|
|
export const isInRange = (value: number, min: number, max: number): boolean => {
|
|
return value >= min && value <= max;
|
|
};
|
|
|
|
/**
|
|
* Check if array has minimum length
|
|
*/
|
|
export const hasMinLength = <T>(array: T[], minLength: number): boolean => {
|
|
return array.length >= minLength;
|
|
};
|
|
|
|
/**
|
|
* Check if array has maximum length
|
|
*/
|
|
export const hasMaxLength = <T>(array: T[], maxLength: number): boolean => {
|
|
return array.length <= maxLength;
|
|
};
|
|
|
|
/**
|
|
* Check if string has minimum length
|
|
*/
|
|
export const hasMinStringLength = (str: string, minLength: number): boolean => {
|
|
return str.length >= minLength;
|
|
};
|
|
|
|
/**
|
|
* Check if string has maximum length
|
|
*/
|
|
export const hasMaxStringLength = (str: string, maxLength: number): boolean => {
|
|
return str.length <= maxLength;
|
|
};
|
|
|
|
/**
|
|
* Check if value is valid JSON string
|
|
*/
|
|
export const isValidJSON = (value: string): boolean => {
|
|
try {
|
|
JSON.parse(value);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if value is positive number
|
|
*/
|
|
export const isPositive = (value: number): boolean => {
|
|
return value > 0;
|
|
};
|
|
|
|
/**
|
|
* Check if value is non-negative number
|
|
*/
|
|
export const isNonNegative = (value: number): boolean => {
|
|
return value >= 0;
|
|
};
|
|
|
|
/**
|
|
* Check if value is negative number
|
|
*/
|
|
export const isNegative = (value: number): boolean => {
|
|
return value < 0;
|
|
};
|
|
|
|
/**
|
|
* Check if string matches pattern
|
|
*/
|
|
export const matchesPattern = (value: string, pattern: RegExp): boolean => {
|
|
return pattern.test(value);
|
|
};
|
|
|
|
/**
|
|
* Validate required fields in object
|
|
*/
|
|
export const hasRequiredFields = <T extends object>(
|
|
obj: T,
|
|
requiredFields: (keyof T)[],
|
|
): boolean => {
|
|
return requiredFields.every(
|
|
(field) => obj[field] !== undefined && obj[field] !== null,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Check if value is valid credit card number (Luhn algorithm)
|
|
*/
|
|
export const isCreditCard = (cardNumber: string): boolean => {
|
|
const sanitized = cardNumber.replace(/\D/g, '');
|
|
if (sanitized.length < 13 || sanitized.length > 19) return false;
|
|
|
|
let sum = 0;
|
|
let isEven = false;
|
|
|
|
for (let i = sanitized.length - 1; i >= 0; i--) {
|
|
let digit = parseInt(sanitized[i], 10);
|
|
|
|
if (isEven) {
|
|
digit *= 2;
|
|
if (digit > 9) digit -= 9;
|
|
}
|
|
|
|
sum += digit;
|
|
isEven = !isEven;
|
|
}
|
|
|
|
return sum % 10 === 0;
|
|
};
|
|
|
|
/**
|
|
* Check if value is valid IP address (v4)
|
|
*/
|
|
export const isIPv4 = (ip: string): boolean => {
|
|
const ipv4Regex =
|
|
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
return ipv4Regex.test(ip);
|
|
};
|
|
|
|
/**
|
|
* Check if value is valid hex color
|
|
*/
|
|
export const isHexColor = (color: string): boolean => {
|
|
const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
|
return hexColorRegex.test(color);
|
|
};
|
|
|
|
/**
|
|
* Check if value is valid slug
|
|
*/
|
|
export const isSlug = (slug: string): boolean => {
|
|
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
return slugRegex.test(slug);
|
|
};
|
|
|
|
/**
|
|
* Check if value is valid username
|
|
* Requirements: 3-30 chars, alphanumeric + underscore, starts with letter
|
|
*/
|
|
export const isValidUsername = (username: string): boolean => {
|
|
const usernameRegex = /^[a-zA-Z][a-zA-Z0-9_]{2,29}$/;
|
|
return usernameRegex.test(username);
|
|
};
|
|
|
|
/**
|
|
* Check if object is empty
|
|
*/
|
|
export const isEmptyObject = (obj: object): boolean => {
|
|
return Object.keys(obj).length === 0;
|
|
};
|
|
|
|
/**
|
|
* Check if array is empty
|
|
*/
|
|
export const isEmptyArray = <T>(arr: T[]): boolean => {
|
|
return arr.length === 0;
|
|
};
|
|
|
|
/**
|
|
* Check if value is null or undefined
|
|
*/
|
|
export const isNullOrUndefined = (value: unknown): value is null | undefined => {
|
|
return value === null || value === undefined;
|
|
};
|
|
|
|
/**
|
|
* Check if value is defined (not null and not undefined)
|
|
*/
|
|
export const isDefined = <T>(value: T | null | undefined): value is T => {
|
|
return value !== null && value !== undefined;
|
|
};
|
|
|
|
/**
|
|
* Validate Mexican RFC (tax ID)
|
|
*/
|
|
export const isMexicanRFC = (rfc: string): boolean => {
|
|
const rfcRegex =
|
|
/^([A-ZÑ&]{3,4})(\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([A-Z\d]{2})([A\d])$/;
|
|
return rfcRegex.test(rfc.toUpperCase());
|
|
};
|
|
|
|
/**
|
|
* Validate Mexican CURP
|
|
*/
|
|
export const isMexicanCURP = (curp: string): boolean => {
|
|
const curpRegex =
|
|
/^[A-Z]{4}\d{6}[HM][A-Z]{5}[A-Z\d]\d$/;
|
|
return curpRegex.test(curp.toUpperCase());
|
|
};
|
|
|
|
/**
|
|
* Validation result type
|
|
*/
|
|
export interface ValidationResult {
|
|
isValid: boolean;
|
|
errors: string[];
|
|
}
|
|
|
|
/**
|
|
* Create a validator chain
|
|
*/
|
|
export const createValidator = <T>(value: T): {
|
|
validate: (condition: boolean, errorMessage: string) => ReturnType<typeof createValidator<T>>;
|
|
result: () => ValidationResult;
|
|
} => {
|
|
const errors: string[] = [];
|
|
|
|
const validator = {
|
|
validate: (condition: boolean, errorMessage: string) => {
|
|
if (!condition) {
|
|
errors.push(errorMessage);
|
|
}
|
|
return validator;
|
|
},
|
|
result: (): ValidationResult => ({
|
|
isValid: errors.length === 0,
|
|
errors,
|
|
}),
|
|
};
|
|
|
|
return validator;
|
|
};
|