UNPKG

@mindmakr/gs-websdk

Version:

Web SDK for Guru SaaS System - Complete JavaScript/TypeScript SDK for building applications with dynamic schema management

781 lines 26 kB
"use strict"; /** * Validation Helper Utilities * Provides comprehensive validation functions for forms and data */ Object.defineProperty(exports, "__esModule", { value: true }); exports.formatValidationErrors = exports.createValidationRules = exports.validateSchema = exports.validateField = exports.sanitizeFormData = exports.validateFieldDependencies = exports.validateForm = void 0; /** * Comprehensive form validation */ function validateForm(formData, template, options = {}) { const { validateRequired = true, validateTypes = true, validateFormats = true, validateConstraints = true, validateCustomRules = true } = options; const errors = []; const warnings = []; const schema = template.schema_definition; if (!schema.properties) { return { isValid: true, errors: [], warnings: [] }; } // Validate required fields if (validateRequired && schema.required) { schema.required.forEach((field) => { if (!hasValue(formData[field])) { errors.push({ field, message: `${getFieldLabel(schema, field)} is required`, code: 'REQUIRED_FIELD', severity: 'error', value: formData[field] }); } }); } // Validate each field Object.entries(schema.properties).forEach(([field, fieldSchema]) => { const value = formData[field]; // Skip validation if field is empty and not required if (!hasValue(value) && (!schema.required || !schema.required.includes(field))) { return; } // Type validation if (validateTypes) { const typeErrors = validateFieldType(field, value, fieldSchema, schema); errors.push(...typeErrors); } // Format validation if (validateFormats) { const formatErrors = validateFieldFormat(field, value, fieldSchema, schema); errors.push(...formatErrors); } // Constraint validation if (validateConstraints) { const constraintErrors = validateFieldConstraints(field, value, fieldSchema, schema); errors.push(...constraintErrors); } // Custom validation rules if (validateCustomRules && fieldSchema.validation) { const customErrors = validateCustomValidationRules(field, value, fieldSchema.validation, formData); errors.push(...customErrors); } // Generate warnings for best practices const fieldWarnings = generateFieldWarnings(field, value, fieldSchema); warnings.push(...fieldWarnings); }); return { isValid: errors.length === 0, errors, warnings }; } exports.validateForm = validateForm; /** * Check if a value is considered "has value" for validation */ function hasValue(value) { if (value === null || value === undefined) return false; if (typeof value === 'string' && value.trim() === '') return false; if (Array.isArray(value) && value.length === 0) return false; return true; } /** * Get field label for error messages */ function getFieldLabel(schema, field) { return schema.properties[field]?.title || field; } /** * Validate field type */ function validateFieldType(field, value, fieldSchema, schema) { const errors = []; const label = getFieldLabel(schema, field); if (!hasValue(value)) return errors; switch (fieldSchema.type) { case 'string': if (typeof value !== 'string') { errors.push({ field, message: `${label} must be a text value`, code: 'INVALID_TYPE_STRING', severity: 'error', value }); } break; case 'number': if (typeof value !== 'number' || isNaN(value)) { errors.push({ field, message: `${label} must be a valid number`, code: 'INVALID_TYPE_NUMBER', severity: 'error', value }); } break; case 'integer': if (!Number.isInteger(Number(value))) { errors.push({ field, message: `${label} must be a whole number`, code: 'INVALID_TYPE_INTEGER', severity: 'error', value }); } break; case 'boolean': if (typeof value !== 'boolean') { errors.push({ field, message: `${label} must be true or false`, code: 'INVALID_TYPE_BOOLEAN', severity: 'error', value }); } break; case 'array': if (!Array.isArray(value)) { errors.push({ field, message: `${label} must be a list of values`, code: 'INVALID_TYPE_ARRAY', severity: 'error', value }); } break; case 'object': if (typeof value !== 'object' || Array.isArray(value)) { errors.push({ field, message: `${label} must be an object`, code: 'INVALID_TYPE_OBJECT', severity: 'error', value }); } break; } return errors; } /** * Validate field format */ function validateFieldFormat(field, value, fieldSchema, schema) { const errors = []; const label = getFieldLabel(schema, field); if (!fieldSchema.format || !hasValue(value) || typeof value !== 'string') { return errors; } const formatValidators = { email: { regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'must be a valid email address' }, url: { regex: /^https?:\/\/.+/, message: 'must be a valid URL starting with http:// or https://' }, uri: { regex: /^https?:\/\/.+/, message: 'must be a valid URI' }, date: { regex: /^\d{4}-\d{2}-\d{2}$/, message: 'must be a valid date in YYYY-MM-DD format' }, time: { regex: /^\d{2}:\d{2}(:\d{2})?$/, message: 'must be a valid time in HH:MM or HH:MM:SS format' }, 'date-time': { regex: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, message: 'must be a valid date-time in ISO format' }, phone: { regex: /^[\+]?[\d\s\-\(\)]+$/, message: 'must be a valid phone number' }, password: { regex: /^.{8,}$/, message: 'must be at least 8 characters long' }, color: { regex: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, message: 'must be a valid hex color code' } }; const validator = formatValidators[fieldSchema.format]; if (validator && !validator.regex.test(value)) { errors.push({ field, message: `${label} ${validator.message}`, code: `INVALID_FORMAT_${fieldSchema.format.toUpperCase()}`, severity: 'error', value }); } // Additional date validation if (fieldSchema.format === 'date' && validator?.regex.test(value)) { const date = new Date(value); if (isNaN(date.getTime())) { errors.push({ field, message: `${label} must be a valid date`, code: 'INVALID_DATE', severity: 'error', value }); } } return errors; } /** * Validate field constraints */ function validateFieldConstraints(field, value, fieldSchema, schema) { const errors = []; const label = getFieldLabel(schema, field); if (!hasValue(value)) return errors; // String constraints if (fieldSchema.type === 'string' && typeof value === 'string') { if (fieldSchema.minLength !== undefined && value.length < fieldSchema.minLength) { errors.push({ field, message: `${label} must be at least ${fieldSchema.minLength} characters long`, code: 'MIN_LENGTH', severity: 'error', value }); } if (fieldSchema.maxLength !== undefined && value.length > fieldSchema.maxLength) { errors.push({ field, message: `${label} must be no more than ${fieldSchema.maxLength} characters long`, code: 'MAX_LENGTH', severity: 'error', value }); } if (fieldSchema.pattern) { const regex = new RegExp(fieldSchema.pattern); if (!regex.test(value)) { errors.push({ field, message: `${label} format is invalid`, code: 'PATTERN_MISMATCH', severity: 'error', value }); } } } // Number constraints if ((fieldSchema.type === 'number' || fieldSchema.type === 'integer') && typeof value === 'number') { if (fieldSchema.minimum !== undefined && value < fieldSchema.minimum) { errors.push({ field, message: `${label} must be at least ${fieldSchema.minimum}`, code: 'MIN_VALUE', severity: 'error', value }); } if (fieldSchema.maximum !== undefined && value > fieldSchema.maximum) { errors.push({ field, message: `${label} must be no more than ${fieldSchema.maximum}`, code: 'MAX_VALUE', severity: 'error', value }); } if (fieldSchema.exclusiveMinimum !== undefined && value <= fieldSchema.exclusiveMinimum) { errors.push({ field, message: `${label} must be greater than ${fieldSchema.exclusiveMinimum}`, code: 'EXCLUSIVE_MIN', severity: 'error', value }); } if (fieldSchema.exclusiveMaximum !== undefined && value >= fieldSchema.exclusiveMaximum) { errors.push({ field, message: `${label} must be less than ${fieldSchema.exclusiveMaximum}`, code: 'EXCLUSIVE_MAX', severity: 'error', value }); } if (fieldSchema.multipleOf !== undefined && value % fieldSchema.multipleOf !== 0) { errors.push({ field, message: `${label} must be a multiple of ${fieldSchema.multipleOf}`, code: 'MULTIPLE_OF', severity: 'error', value }); } } // Array constraints if (fieldSchema.type === 'array' && Array.isArray(value)) { if (fieldSchema.minItems !== undefined && value.length < fieldSchema.minItems) { errors.push({ field, message: `${label} must have at least ${fieldSchema.minItems} items`, code: 'MIN_ITEMS', severity: 'error', value }); } if (fieldSchema.maxItems !== undefined && value.length > fieldSchema.maxItems) { errors.push({ field, message: `${label} must have no more than ${fieldSchema.maxItems} items`, code: 'MAX_ITEMS', severity: 'error', value }); } if (fieldSchema.uniqueItems && value.length !== new Set(value).size) { errors.push({ field, message: `${label} must contain unique items only`, code: 'UNIQUE_ITEMS', severity: 'error', value }); } } // Enum validation if (fieldSchema.enum && !fieldSchema.enum.includes(value)) { const allowedValues = fieldSchema.enumNames || fieldSchema.enum; errors.push({ field, message: `${label} must be one of: ${allowedValues.join(', ')}`, code: 'INVALID_ENUM', severity: 'error', value }); } return errors; } /** * Validate custom validation rules */ function validateCustomValidationRules(field, value, validationRules, formData) { const errors = []; if (!Array.isArray(validationRules)) { return errors; } validationRules.forEach(rule => { const error = validateCustomRule(field, value, rule, formData); if (error) { errors.push(error); } }); return errors; } /** * Validate single custom rule */ function validateCustomRule(field, value, rule, formData) { const message = rule.message.en || rule.message.default || 'Validation failed'; switch (rule.type) { case 'required': if (!hasValue(value)) { return { field, message, code: 'CUSTOM_REQUIRED', severity: 'error', value }; } break; case 'min_length': if (typeof value === 'string' && value.length < rule.value) { return { field, message, code: 'CUSTOM_MIN_LENGTH', severity: 'error', value }; } break; case 'max_length': if (typeof value === 'string' && value.length > rule.value) { return { field, message, code: 'CUSTOM_MAX_LENGTH', severity: 'error', value }; } break; case 'regex': if (typeof value === 'string' && !new RegExp(rule.value).test(value)) { return { field, message, code: 'CUSTOM_REGEX', severity: 'error', value }; } break; case 'equals': if (value !== rule.value) { return { field, message, code: 'CUSTOM_EQUALS', severity: 'error', value }; } break; case 'not_equals': if (value === rule.value) { return { field, message, code: 'CUSTOM_NOT_EQUALS', severity: 'error', value }; } break; case 'field_equals': if (value !== formData[rule.value]) { return { field, message, code: 'CUSTOM_FIELD_EQUALS', severity: 'error', value }; } break; case 'field_not_equals': if (value === formData[rule.value]) { return { field, message, code: 'CUSTOM_FIELD_NOT_EQUALS', severity: 'error', value }; } break; case 'contains': if (typeof value === 'string' && !value.includes(rule.value)) { return { field, message, code: 'CUSTOM_CONTAINS', severity: 'error', value }; } break; case 'not_contains': if (typeof value === 'string' && value.includes(rule.value)) { return { field, message, code: 'CUSTOM_NOT_CONTAINS', severity: 'error', value }; } break; case 'in_array': if (!Array.isArray(rule.value) || !rule.value.includes(value)) { return { field, message, code: 'CUSTOM_IN_ARRAY', severity: 'error', value }; } break; case 'not_in_array': if (Array.isArray(rule.value) && rule.value.includes(value)) { return { field, message, code: 'CUSTOM_NOT_IN_ARRAY', severity: 'error', value }; } break; } return null; } /** * Generate warnings for best practices */ function generateFieldWarnings(field, value, fieldSchema) { const warnings = []; // Password strength warning if (fieldSchema.format === 'password' && typeof value === 'string') { if (value.length < 12) { warnings.push({ field, message: 'Consider using a longer password for better security', code: 'WEAK_PASSWORD_LENGTH', value }); } if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/.test(value)) { warnings.push({ field, message: 'Consider including uppercase, lowercase, numbers, and special characters', code: 'WEAK_PASSWORD_COMPLEXITY', value }); } } // Email domain warning if (fieldSchema.format === 'email' && typeof value === 'string') { const domain = value.split('@')[1]; if (domain && ['test.com', 'example.com', 'temp.com'].includes(domain.toLowerCase())) { warnings.push({ field, message: 'This appears to be a temporary or test email address', code: 'SUSPICIOUS_EMAIL_DOMAIN', value }); } } // URL security warning if (fieldSchema.format === 'url' && typeof value === 'string') { if (value.startsWith('http://')) { warnings.push({ field, message: 'Consider using HTTPS for better security', code: 'INSECURE_URL', value }); } } // Large file warning (for file uploads) if (fieldSchema.format === 'data-url' && typeof value === 'string') { // Estimate file size from base64 data URL const sizeEstimate = (value.length * 3) / 4; if (sizeEstimate > 5 * 1024 * 1024) { // 5MB warnings.push({ field, message: 'Large file detected. Consider optimizing for better performance', code: 'LARGE_FILE', value }); } } return warnings; } /** * Validate field dependencies and cross-field validation */ function validateFieldDependencies(formData, template) { const errors = []; const schema = template.schema_definition; if (!schema.properties) { return errors; } // Check conditional field dependencies Object.entries(schema.properties).forEach(([field, fieldSchema]) => { if (fieldSchema.dependencies) { Object.entries(fieldSchema.dependencies).forEach(([depField, depValue]) => { if (formData[depField] === depValue && !hasValue(formData[field])) { errors.push({ field, message: `${field} is required when ${depField} is ${depValue}`, code: 'DEPENDENCY_REQUIRED', severity: 'error', value: formData[field] }); } }); } }); return errors; } exports.validateFieldDependencies = validateFieldDependencies; /** * Sanitize form data (remove potentially harmful content) */ function sanitizeFormData(formData, options = {}) { const { stripHtml = true, stripScripts = true, maxStringLength = 10000 } = options; function sanitizeValue(value) { if (typeof value === 'string') { let sanitized = value; // Strip scripts if (stripScripts) { sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); sanitized = sanitized.replace(/javascript:/gi, ''); sanitized = sanitized.replace(/on\w+\s*=/gi, ''); } // Strip HTML if (stripHtml) { sanitized = sanitized.replace(/<[^>]*>/g, ''); } // Truncate if too long if (sanitized.length > maxStringLength) { sanitized = sanitized.substring(0, maxStringLength); } return sanitized; } if (Array.isArray(value)) { return value.map(sanitizeValue); } if (value && typeof value === 'object') { const sanitizedObj = {}; Object.entries(value).forEach(([key, val]) => { sanitizedObj[key] = sanitizeValue(val); }); return sanitizedObj; } return value; } const sanitized = {}; Object.entries(formData).forEach(([key, value]) => { sanitized[key] = sanitizeValue(value); }); return sanitized; } exports.sanitizeFormData = sanitizeFormData; /** * Validate a single field */ function validateField(field, value, fieldSchema, formData) { const errors = []; const warnings = []; // Type validation const typeErrors = validateFieldType(field, value, fieldSchema, { properties: { [field]: fieldSchema } }); errors.push(...typeErrors); // Format validation const formatErrors = validateFieldFormat(field, value, fieldSchema, { properties: { [field]: fieldSchema } }); errors.push(...formatErrors); // Constraint validation const constraintErrors = validateFieldConstraints(field, value, fieldSchema, { properties: { [field]: fieldSchema } }); errors.push(...constraintErrors); // Custom validation rules if (fieldSchema.validation) { const customErrors = validateCustomValidationRules(field, value, fieldSchema.validation, formData || {}); errors.push(...customErrors); } return { isValid: errors.length === 0, errors, warnings }; } exports.validateField = validateField; /** * Validate a schema structure */ function validateSchema(schema) { const errors = []; const warnings = []; if (!schema) { errors.push({ field: 'schema', message: 'Schema is required', code: 'SCHEMA_REQUIRED', severity: 'error' }); return { isValid: false, errors, warnings }; } if (schema.type !== 'object') { errors.push({ field: 'schema.type', message: 'Schema type must be "object"', code: 'INVALID_SCHEMA_TYPE', severity: 'error' }); } if (!schema.properties) { errors.push({ field: 'schema.properties', message: 'Schema must have properties', code: 'SCHEMA_PROPERTIES_REQUIRED', severity: 'error' }); } return { isValid: errors.length === 0, errors, warnings }; } exports.validateSchema = validateSchema; /** * Create validation rules for a field */ function createValidationRules(fieldType, constraints = {}) { const rules = []; // Add basic type validation rules.push({ type: 'type', value: fieldType, message: { en: `Field must be of type ${fieldType}`, default: `Field must be of type ${fieldType}` } }); // Add constraint-based rules if (constraints.required) { rules.push({ type: 'required', value: true, message: { en: 'This field is required', default: 'This field is required' } }); } if (constraints.minLength) { rules.push({ type: 'minLength', value: constraints.minLength, message: { en: `Must be at least ${constraints.minLength} characters`, default: `Must be at least ${constraints.minLength} characters` } }); } if (constraints.maxLength) { rules.push({ type: 'maxLength', value: constraints.maxLength, message: { en: `Must be no more than ${constraints.maxLength} characters`, default: `Must be no more than ${constraints.maxLength} characters` } }); } return rules; } exports.createValidationRules = createValidationRules; /** * Format validation errors for display */ function formatValidationErrors(errors) { return errors.map(error => error.message); } exports.formatValidationErrors = formatValidationErrors; //# sourceMappingURL=validation-helpers.js.map