@wearesage/schema
Version:
A flexible schema definition and validation system for TypeScript with multi-database support
201 lines (169 loc) • 6.24 kB
text/typescript
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();