UNPKG

@wearesage/schema

Version:

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

205 lines (169 loc) 6.59 kB
import { DatabaseAdapter } from './interface'; import { MetadataRegistry } from '../core/MetadataRegistry'; import { Type } from '../core/types'; import { embeddingService } from './embeddings'; /** * Configuration for metadata layer fields */ export interface MetadataConfig { fields: string[]; tableName?: string; autoSync?: boolean; } /** * Hybrid adapter that combines a primary adapter (for rich data/relationships) * with a metadata adapter (for fast lookups/indexing) */ export class MetadataLayerAdapter implements DatabaseAdapter { readonly type: string = 'metadata-layer'; constructor( private primaryAdapter: DatabaseAdapter, // e.g., Neo4j for rich graph data private metadataAdapter: DatabaseAdapter, // e.g., Postgres for fast lookups private registry: MetadataRegistry ) {} /** * Save entity to primary adapter, then sync metadata to secondary */ async save<T extends object>(entity: T): Promise<void> { // Generate embeddings if configured try { await embeddingService.generateEmbeddings(entity); console.log('🧠 Generated embeddings for entity'); } catch (error) { console.warn('⚠️ Failed to generate embeddings:', error); // Continue with save even if embeddings fail } // Save to primary adapter first (Neo4j) await this.primaryAdapter.save(entity); // Extract and sync metadata to secondary adapter (Postgres) await this.syncMetadata(entity); } /** * Route queries based on complexity and metadata configuration */ async query<T>(entityType: Type<T>, criteria: object): Promise<T | null> { const metadataConfig = this.getMetadataConfig(entityType); // If no metadata config, use primary adapter if (!metadataConfig) { return this.primaryAdapter.query(entityType, criteria); } // If this is a simple lookup query (only uses metadata fields), use metadata adapter if (this.isSimpleQuery(criteria, metadataConfig.fields)) { const metadataResult = await this.metadataAdapter.query(entityType, criteria); // If we need full entity, hydrate from primary using the ID if (metadataResult && this.needsHydration(criteria)) { return this.primaryAdapter.query(entityType, { id: (metadataResult as any).id }); } return metadataResult; } // Complex queries go to primary adapter return this.primaryAdapter.query(entityType, criteria); } /** * Route queryMany based on complexity */ async queryMany<T>(entityType: Type<T>, criteria: object): Promise<T[]> { const metadataConfig = this.getMetadataConfig(entityType); // If no metadata config, use primary adapter if (!metadataConfig) { return this.primaryAdapter.queryMany(entityType, criteria); } // Simple queries hit metadata layer for speed if (this.isSimpleQuery(criteria, metadataConfig.fields)) { return this.metadataAdapter.queryMany(entityType, criteria); } // Complex queries go to primary return this.primaryAdapter.queryMany(entityType, criteria); } /** * Delete from both adapters (if metadata config exists) */ async delete<T>(entityType: Type<T>, id: string | number): Promise<void> { const metadataConfig = this.getMetadataConfig(entityType); if (metadataConfig) { // Delete from both adapters if metadata sync is configured await Promise.all([ this.primaryAdapter.delete(entityType, id), this.metadataAdapter.delete(entityType, id) ]); } else { // Only delete from primary adapter if no metadata config await this.primaryAdapter.delete(entityType, id); } } /** * Run native query on primary adapter (for complex operations) */ async runNativeQuery<T>(query: string, params?: any): Promise<T> { return this.primaryAdapter.runNativeQuery(query, params); } /** * Sync metadata fields to secondary adapter */ private async syncMetadata<T extends object>(entity: T): Promise<void> { const entityType = entity.constructor as Type<T>; const metadataConfig = this.getMetadataConfig(entityType); if (!metadataConfig) { return; // No metadata config, skip sync } // Extract only the configured metadata fields const metadataFields = this.extractMetadataFields(entity, metadataConfig.fields); // Create a minimal metadata entity instance that bypasses validation const metadataEntity = Object.create(entityType.prototype); Object.assign(metadataEntity, metadataFields); // Use the adapter's save method - let it handle the database-specific logic await this.metadataAdapter.save(metadataEntity); } /** * Extract specified fields from entity for metadata storage */ private extractMetadataFields<T extends object>(entity: T, fields: string[]): Partial<T> { const metadata: any = {}; for (const field of fields) { if (field in entity) { metadata[field] = (entity as any)[field]; } } return metadata; } /** * Check if query only uses metadata fields (can be routed to fast adapter) */ private isSimpleQuery(criteria: object, metadataFields?: string[]): boolean { if (!metadataFields) { return false; } const queryFields = Object.keys(criteria); // All query fields must be in metadata fields return queryFields.every(field => metadataFields.includes(field)); } /** * Check if result needs hydration from primary adapter */ private needsHydration(criteria: object): boolean { // For now, simple heuristic: if criteria includes relationships or complex fields // In the future, this could be more sophisticated const complexFields = ['include', 'relations', 'with']; return complexFields.some(field => field in criteria); } /** * Get metadata configuration for entity type */ private getMetadataConfig(entityType: Type<any>): MetadataConfig | undefined { return Reflect.getMetadata('postgres:metadata', entityType); } } /** * Decorator for configuring PostgreSQL metadata layer */ export function PostgresMetadata(config?: Partial<MetadataConfig>) { return function(target: any) { const defaultConfig: MetadataConfig = { fields: ['id', 'name', 'createdAt', 'updatedAt'], tableName: target.name.toLowerCase() + 's', autoSync: true }; const finalConfig = { ...defaultConfig, ...config }; Reflect.defineMetadata('postgres:metadata', finalConfig, target); }; }