@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
JavaScript
"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