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