All files / core ValidationEngine.ts

90.9% Statements 80/88
63.41% Branches 26/41
100% Functions 16/16
97.43% Lines 76/78

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 20114x                 14x 30x     30x             21x 21x     21x   21x 51x 51x     21x                   62x 62x 62x     62x   62x 83x 83x   83x 22x                                     62x                   83x   83x       83x             271x             21x 21x     21x 21x   21x   21x 82x   61x     61x 51x       21x               30x 18x 16x 16x       30x 14x 14x 14x       30x 2x 2x       30x 16x 14x 14x       30x 6x 6x 6x 4x 4x   2x         30x 2x 2x       30x 16x 14x 14x       30x 6x 6x 6x       30x 2x 2x 2x 2x 2x           14x
import "reflect-metadata";
import type { ValidationError, ValidationResult, ValidationRule, ValidatorFunction } from "./types";
 
/**
 * ValidationEngine - Executes validation rules stored by decorators
 * 
 * This engine reads validation metadata stored by decorators like @Min, @Max, @Email
 * and executes them against entity instances to produce validation results.
 */
export class ValidationEngine {
  private validators: Map<string, ValidatorFunction> = new Map();
 
  constructor() {
    this.registerBuiltinValidators();
  }
 
  /**
   * Validates an entity instance against all its validation rules
   */
  async validate(entity: any): Promise<ValidationResult> {
    const errors: ValidationError[] = [];
    const target = entity.constructor.name;
 
    // Get all properties that have validation rules
    const properties = this.getPropertiesWithValidation(entity);
 
    for (const propertyKey of properties) {
      const propertyResult = await this.validateProperty(entity, propertyKey, target);
      errors.push(...propertyResult.errors);
    }
 
    return {
      isValid: errors.length === 0,
      errors,
    };
  }
 
  /**
   * Validates a specific property of an entity
   */
  async validateProperty(entity: any, propertyKey: string, target?: string): Promise<ValidationResult> {
    const errors: ValidationError[] = [];
    const entityTarget = target || entity.constructor.name;
    const value = entity[propertyKey];
 
    // Get validation rules for this property
    const rules: ValidationRule[] = Reflect.getMetadata("validation:rules", entity, propertyKey) || [];
 
    for (const rule of rules) {
      try {
        const isValid = await this.executeRule(value, rule);
        
        if (!isValid) {
          errors.push({
            property: propertyKey,
            value,
            message: rule.message || `Validation failed for ${propertyKey}`,
            rule: rule.type,
            target: entityTarget,
          });
        }
      } catch (error) {
        errors.push({
          property: propertyKey,
          value,
          message: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`,
          rule: rule.type,
          target: entityTarget,
        });
      }
    }
 
    return {
      isValid: errors.length === 0,
      errors,
    };
  }
 
  /**
   * Executes a single validation rule
   */
  private async executeRule(value: any, rule: ValidationRule): Promise<boolean> {
    const validator = this.validators.get(rule.type);
    
    Iif (!validator) {
      throw new Error(`Unknown validation rule: ${rule.type}`);
    }
 
    return await validator(value, rule.options);
  }
 
  /**
   * Registers a custom validator function
   */
  registerValidator(type: string, validator: ValidatorFunction): void {
    this.validators.set(type, validator);
  }
 
  /**
   * Gets all properties that have validation metadata
   */
  private getPropertiesWithValidation(entity: any): string[] {
    const properties: string[] = [];
    const prototype = Object.getPrototypeOf(entity);
 
    // Get all own property names
    const ownProps = Object.getOwnPropertyNames(entity);
    const prototypeProps = Object.getOwnPropertyNames(prototype);
    
    const allProps = [...new Set([...ownProps, ...prototypeProps])];
 
    for (const prop of allProps) {
      if (prop === 'constructor') continue;
      
      const hasValidationRules = Reflect.hasMetadata("validation:rules", entity, prop) ||
                                Reflect.hasMetadata("validation:rules", prototype, prop);
      
      if (hasValidationRules) {
        properties.push(prop);
      }
    }
 
    return properties;
  }
 
  /**
   * Registers all built-in validators
   */
  private registerBuiltinValidators(): void {
    // Min validator
    this.registerValidator("min", (value: any, options: any) => {
      if (value == null) return true; // null/undefined values are handled by required validation
      const num = Number(value);
      return !isNaN(num) && num >= options.value;
    });
 
    // Max validator
    this.registerValidator("max", (value: any, options: any) => {
      Iif (value == null) return true;
      const num = Number(value);
      return !isNaN(num) && num <= options.value;
    });
 
    // Pattern validator
    this.registerValidator("pattern", (value: any, options: any) => {
      Iif (value == null) return true;
      return options.pattern.test(String(value));
    });
 
    // Email validator
    this.registerValidator("email", (value: any, options: any) => {
      if (value == null) return true;
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return emailRegex.test(String(value));
    });
 
    // URL validator
    this.registerValidator("url", (value: any, options: any) => {
      Iif (value == null) return true;
      try {
        const url = new URL(String(value));
        const protocols = options.protocols || ['http:', 'https:'];
        return protocols.includes(url.protocol);
      } catch {
        return false;
      }
    });
 
    // Custom validator
    this.registerValidator("custom", async (value: any, options: any) => {
      Iif (value == null) return true;
      return await options.validator(value);
    });
 
    // MinLength validator
    this.registerValidator("minLength", (value: any, options: any) => {
      if (value == null) return true;
      const length = Array.isArray(value) ? value.length : String(value).length;
      return length >= options.value;
    });
 
    // MaxLength validator
    this.registerValidator("maxLength", (value: any, options: any) => {
      Iif (value == null) return true;
      const length = Array.isArray(value) ? value.length : String(value).length;
      return length <= options.value;
    });
 
    // Length validator (range)
    this.registerValidator("length", (value: any, options: any) => {
      Iif (value == null) return true;
      const length = Array.isArray(value) ? value.length : String(value).length;
      const min = options.min || 0;
      const max = options.max || Infinity;
      return length >= min && length <= max;
    });
  }
}
 
// Export a default instance for convenience
export const defaultValidationEngine = new ValidationEngine();