UNPKG

@wearesage/schema

Version:

A flexible schema definition and validation system for TypeScript with multi-database support

201 lines (169 loc) 6.24 kB
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); if (!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) => { if (value == null) return true; const num = Number(value); return !isNaN(num) && num <= options.value; }); // Pattern validator this.registerValidator("pattern", (value: any, options: any) => { if (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) => { if (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) => { if (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) => { if (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) => { if (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();