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