UNPKG

@wearesage/schema

Version:

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

168 lines (146 loc) 6.06 kB
import { MetadataRegistry } from "./MetadataRegistry"; import { ValidationEngine, defaultValidationEngine } from "./ValidationEngine"; import type { ValidationResult } from "./types"; import 'reflect-metadata'; /** * SchemaReflector provides utilities for inspecting and validating * schema metadata from entity classes. */ export class SchemaReflector { /** * Creates a new SchemaReflector * @param registry The metadata registry to use (if not provided, a new one will be created) * @param validationEngine The validation engine to use (if not provided, the default one will be used) */ constructor( private registry: MetadataRegistry = new MetadataRegistry(), private validationEngine: ValidationEngine = defaultValidationEngine ) {} getEntitySchema(entity: Function): any { const metadata = this.registry.getEntityMetadata(entity); if (!metadata) { throw new Error(`Class ${entity.name} is not decorated as an Entity`); } const properties = this.registry.getAllProperties(entity) || new Map(); const idProperties = this.registry.getIdProperties(entity) || new Set(); const relationships = this.registry.getAllRelationships(entity) || new Map(); // Get auth metadata const authOptions = Reflect.getMetadata("auth:options", entity) || {}; // Build schema representation return { name: metadata.name || entity.name, description: metadata.description, properties: Object.fromEntries(properties), idProperties: Array.from(idProperties), relationships: Object.fromEntries(relationships), auth: authOptions, }; } validateEntity(entity: any): { valid: boolean; errors: string[] } { const constructor = entity.constructor; const errors: string[] = []; // Check if entity is decorated const isEntity = Reflect.hasMetadata("entity:options", constructor); if (!isEntity) { return { valid: false, errors: ["Entity class is not properly decorated"], }; } // Get all properties from reflect-metadata const registeredProperties: string[] = Reflect.getMetadata("entity:properties", constructor) || []; // Validate required properties for (const propertyKey of registeredProperties) { const propertyOptions = Reflect.getMetadata("property:options", entity, propertyKey); if ( propertyOptions?.required && (entity[propertyKey] === undefined || entity[propertyKey] === null) ) { errors.push(`Required property '${propertyKey}' is missing`); } } return { valid: errors.length === 0, errors, }; } /** * Validates an entity using the new ValidationEngine * This method uses all validation decorators (@Min, @Max, @Email, etc.) */ async validateEntityWithRules(entity: any): Promise<ValidationResult> { // First run basic validation (required properties) const basicValidation = this.validateEntity(entity); // Then run validation rules const ruleValidation = await this.validationEngine.validate(entity); // Combine results const allErrors = [ ...basicValidation.errors.map(error => ({ property: 'unknown', value: undefined, message: error, rule: 'required', target: entity.constructor.name, })), ...ruleValidation.errors, ]; return { isValid: basicValidation.valid && ruleValidation.isValid, errors: allErrors, }; } /** * Validates a specific property of an entity */ async validateProperty(entity: any, propertyKey: string): Promise<ValidationResult> { return await this.validationEngine.validateProperty(entity, propertyKey); } /** * Infer relationship type based on property type * This method can be used to automatically determine relationship types * based on TypeScript's metadata when using TypeScript with emitDecoratorMetadata * * @param target The target object (prototype) * @param propertyKey The property name * @returns The inferred relationship type or undefined if it cannot be determined */ inferRelationshipType(target: any, propertyKey: string): 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many' | undefined { // First check if explicit relationship type is defined via decorators const explicitType = Reflect.getMetadata('relationship:type', target, propertyKey); if (explicitType) { return explicitType as any; } // Try to infer from property design type (requires emitDecoratorMetadata in tsconfig) const designType = Reflect.getMetadata('design:type', target, propertyKey); if (designType) { // Check if it's an array if (designType === Array) { // It's a "to-many" relationship, but we can't distinguish // between one-to-many and many-to-many without more context return undefined; } else if (designType.prototype && this.registry.getEntityMetadata(designType)) { // It's a "to-one" relationship to an entity, but we can't distinguish // between one-to-one and many-to-one without more context return undefined; } } return undefined; } /** * Automatically detect and register relationships from property types * This method would be used to enhance the reflection capabilities in the future * * @param entityClass The entity class to analyze */ detectRelationships(entityClass: Function): void { // This is a placeholder for future implementation // It would scan all properties of the class, and for those that are entities // or arrays of entities, it would register appropriate relationships // Implementation would require: // 1. Get all properties from class prototype // 2. For each property, check if it's an entity type or array of entity type // 3. Register appropriate relationship metadata // Note: This would require emitDecoratorMetadata and appropriate tsconfig settings } }