UNPKG

@wearesage/schema

Version:

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

321 lines (276 loc) 9.7 kB
import { Type } from "../core/types"; import { DatabaseAdapter } from "./interface"; import { MetadataRegistry } from "../core/MetadataRegistry"; import { SchemaReflector } from "../core/SchemaReflector"; /** * Redis-specific key decorator */ export function Key(prefix: string) { return function (target: any) { Reflect.defineMetadata("redis:key", prefix, target); }; } /** * Redis-specific TTL decorator for entity or property expiration */ export function TTL(seconds: number) { return function (target: any, propertyKey?: string) { if (!propertyKey) { // Applied to class - global TTL for all instances Reflect.defineMetadata("redis:ttl", seconds, target); } else { // Applied to property - property-specific TTL const ttlMap = Reflect.getMetadata("redis:property:ttl", target.constructor) || new Map(); ttlMap.set(propertyKey, seconds); Reflect.defineMetadata("redis:property:ttl", ttlMap, target.constructor); } }; } /** * Redis-specific index decorator for secondary indexes */ export function Index(name: string) { return function (target: any, propertyKey: string) { const indexes = Reflect.getMetadata("redis:indexes", target.constructor) || []; indexes.push({ name, field: propertyKey }); Reflect.defineMetadata("redis:indexes", indexes, target.constructor); }; } /** * Shorthand decorator for Redis entities */ export function Redis<T extends { new (...args: any[]): any }>(target: T): T { return DatabaseAdapter("Redis")(target); } /** * Implementation of DatabaseAdapter for Redis */ export class RedisAdapter implements DatabaseAdapter { readonly type = "Redis"; private registry: MetadataRegistry; private reflector = new SchemaReflector(); /** * Constructor for Redis adapter * @param registry The metadata registry to use for schema information * @param connectionString The Redis connection string */ constructor(registry: MetadataRegistry, private connectionString: string) { this.registry = registry; } /** * Get the key prefix for an entity type */ private getKeyPrefix<T>(entityType: Type<T>): string { // Get custom key prefix if defined, otherwise use entity name const keyPrefix = Reflect.getMetadata("redis:key", entityType); if (keyPrefix) { return keyPrefix; } return entityType.name.toLowerCase(); } /** * Get the full Redis key for an entity */ private getEntityKey<T>(entityType: Type<T>, id: string | number): string { const prefix = this.getKeyPrefix(entityType); return `${prefix}:${id}`; } /** * Get TTL for an entity type or property */ private getTTL<T>(entityType: Type<T>, propertyKey?: string): number | null { if (propertyKey) { const propertyTTLs = Reflect.getMetadata( "redis:property:ttl", entityType ); if (propertyTTLs && propertyTTLs.has(propertyKey)) { return propertyTTLs.get(propertyKey); } } // Fall back to entity-level TTL return Reflect.getMetadata("redis:ttl", entityType) || null; } /** * Get all defined indexes for an entity type */ private getIndexes<T>(entityType: Type<T>): any[] { return Reflect.getMetadata("redis:indexes", entityType) || []; } /** * Convert entity to Redis hash */ private entityToHash<T>(entity: T): Record<string, string> { const entityType = entity.constructor as Type<T>; const entitySchema = this.reflector.getEntitySchema(entityType); const hash: Record<string, string> = {}; // Convert each property to string (Redis hash values must be strings) for (const [key, meta] of Object.entries(entitySchema.properties)) { if ((entity as any)[key] !== undefined) { const value = (entity as any)[key]; // Serialize non-string values to JSON hash[key] = typeof value === "string" ? value : JSON.stringify(value); } } return hash; } /** * Convert Redis hash to entity instance */ private hashToEntity<T>( entityType: Type<T>, hash: Record<string, string> ): T { const entity = new entityType(); const entitySchema = this.reflector.getEntitySchema(entityType); // Deserialize each property according to its type for (const [key, meta] of Object.entries(entitySchema.properties)) { if (hash[key] !== undefined) { try { // Try to parse as JSON first (entity as any)[key] = JSON.parse(hash[key]); } catch (e) { // If parsing fails, use the raw string (entity as any)[key] = hash[key]; } } } return entity; } /** * Query for a single entity by criteria */ async query<T>(entityType: Type<T>, criteria: object): Promise<T | null> { console.log(`[RedisAdapter] Querying with criteria:`, criteria); const criteriaObj = criteria as Record<string, any>; // In Redis, efficient queries are typically only by primary key if ("id" in criteriaObj) { const key = this.getEntityKey(entityType, criteriaObj.id); console.log(`[RedisAdapter] Fetching hash at key: ${key}`); // Simulate database call - in a real implementation, would use Redis client // const hash = await redis.hgetall(key); const result = await this.mockQueryExecution<T>(entityType, criteria); return result; } // For more complex criteria, we can use secondary indexes if defined // This would be much more complex in a real implementation // and would likely involve scanning multiple keys or using Redis search return null; } /** * Query for multiple entities by criteria */ async queryMany<T>(entityType: Type<T>, criteria: object): Promise<T[]> { console.log( `[RedisAdapter] Querying for multiple entities with criteria:`, criteria ); // Get all indexes for this entity type const indexes = this.getIndexes(entityType); // In a real implementation, this would use Redis search capabilities // or potentially scan multiple keys based on index patterns // Mock implementation return []; // Mock empty result } /** * Save an entity to Redis */ async save<T extends object>(entity: T): Promise<void> { const entityType = entity.constructor as Type<T>; // Validate the entity before saving const validation = this.reflector.validateEntity(entity); if (!validation.valid) { throw new Error(`Invalid entity: ${validation.errors.join(", ")}`); } // Get entity ID (required) if (!("id" in entity)) { throw new Error("Entity must have an id property to be saved to Redis"); } const id = (entity as any).id; const key = this.getEntityKey(entityType, id); // Convert entity to hash const hash = this.entityToHash(entity); console.log(`[RedisAdapter] Saving hash to key ${key}:`, hash); // In a real implementation, would use Redis client // await redis.hset(key, hash); // Apply TTL if specified const ttl = this.getTTL(entityType); if (ttl !== null) { console.log( `[RedisAdapter] Setting TTL of ${ttl} seconds for key ${key}` ); // await redis.expire(key, ttl); } // Update indexes const indexes = this.getIndexes(entityType); for (const index of indexes) { const indexValue = (entity as any)[index.field]; if (indexValue !== undefined) { const indexKey = `${this.getKeyPrefix(entityType)}:index:${ index.name }:${indexValue}`; console.log( `[RedisAdapter] Updating index at ${indexKey} to point to ${key}` ); // await redis.set(indexKey, key); } } } /** * Delete an entity from Redis */ async delete<T>(entityType: Type<T>, id: string | number): Promise<void> { const key = this.getEntityKey(entityType, id); console.log(`[RedisAdapter] Deleting key ${key}`); // In a real implementation, would use Redis client // await redis.del(key); // Also need to clean up any indexes // This would require first fetching the entity to get indexed values // Simplified example for index cleanup // const indexes = this.getIndexes(entityType); // for (const index of indexes) { // const entity = await this.query(entityType, { id }); // if (entity) { // const indexValue = (entity as any)[index.field]; // if (indexValue !== undefined) { // const indexKey = `${this.getKeyPrefix(entityType)}:index:${index.name}:${indexValue}`; // await redis.del(indexKey); // } // } // } } /** * Execute a raw Redis command */ async runNativeQuery<T>(command: string, args?: any[]): Promise<T> { console.log(`[RedisAdapter] Running native command: ${command}`); console.log(`[RedisAdapter] Args:`, args); // In a real implementation, this would be: // return redis.sendCommand(command, args); return {} as T; // Mock result } /** * Mock method to simulate database query execution * In a real implementation, this would use the Redis client */ private async mockQueryExecution<T>( entityType: Type<T>, criteria: object ): Promise<T | null> { // Just a mock implementation for demonstration const criteriaObj = criteria as Record<string, any>; if ("id" in criteriaObj && criteriaObj.id === "123") { const entity = new entityType(); Object.assign(entity, { id: "123", name: "Mock Redis Entity", createdAt: new Date().toISOString(), }); return entity; } return null; } }