import neo4j, { Driver, Session, SessionConfig } from 'neo4j-driver'; /** * Neo4jService - Hybrid Cloud/Local Graph Database Connection * * Automatically switches between: * - LOCAL (dev): bolt://localhost:7687 or Docker neo4j:7687 * - CLOUD (prod): neo4j+s://.databases.neo4j.io (AuraDB) * * Features: * - Self-healing with automatic reconnection * - Connection pooling * - Health checks * - Singleton pattern */ export class Neo4jService { private driver: Driver | null = null; private isConnecting: boolean = false; private reconnectAttempts: number = 0; private maxReconnectAttempts: number = 10; private reconnectDelay: number = 5000; constructor() { this.connect(); } /** * Determines connection URI based on environment */ private getConnectionConfig(): { uri: string; user: string; password: string } { const isProduction = process.env.NODE_ENV === 'production'; const hasCloudUri = process.env.NEO4J_URI?.includes('neo4j.io'); // Cloud (AuraDB) - when explicitly configured or in production with cloud URI if (hasCloudUri) { console.log('đŸŒŠī¸ Neo4j Mode: CLOUD (AuraDB)'); return { uri: process.env.NEO4J_URI!, user: process.env.NEO4J_USER || 'neo4j', password: process.env.NEO4J_PASSWORD || '' }; } // Local Docker (default for dev) console.log('đŸŗ Neo4j Mode: LOCAL (Docker)'); return { uri: process.env.NEO4J_URI || 'bolt://localhost:7687', user: process.env.NEO4J_USER || 'neo4j', password: process.env.NEO4J_PASSWORD || 'password' }; } /** * Initializes connection with self-healing retry logic */ private async connect(): Promise { if (this.driver || this.isConnecting) return; this.isConnecting = true; const config = this.getConnectionConfig(); try { console.log(`🔌 Connecting to Neural Graph at ${config.uri}...`); this.driver = neo4j.driver( config.uri, neo4j.auth.basic(config.user, config.password), { maxConnectionLifetime: 3 * 60 * 60 * 1000, // 3 hours maxConnectionPoolSize: 50, connectionAcquisitionTimeout: 10000, // 10 seconds connectionTimeout: 30000, // 30 seconds } ); // Verify connectivity await this.driver.verifyConnectivity(); console.log('đŸŸĸ NEURAL CORTEX CONNECTED - Neo4j is Online'); this.reconnectAttempts = 0; this.isConnecting = false; } catch (error: any) { console.error('🔴 Failed to connect to Neural Graph:', error.message); this.driver = null; this.isConnecting = false; // Self-healing: Retry with exponential backoff if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1); console.log(`âŗ Retry attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay/1000}s...`); setTimeout(() => this.connect(), delay); } else { console.error('💀 Max reconnection attempts reached. Neural Graph is OFFLINE.'); } } } /** * Get a session for graph operations * Triggers reconnect if disconnected */ public getSession(config?: SessionConfig): Session { if (!this.driver) { this.connect(); throw new Error('Neural Graph is currently offline. Reconnection in progress...'); } return this.driver.session(config); } /** * Execute a Cypher query with automatic session management */ public async query(cypher: string, params: Record = {}): Promise { const session = this.getSession(); try { const result = await session.run(cypher, params); return result.records.map(record => record.toObject() as T); } finally { await session.close(); } } /** * Execute a write transaction */ public async write(cypher: string, params: Record = {}): Promise { const session = this.getSession(); try { const result = await session.executeWrite(tx => tx.run(cypher, params)); return result.records.map(record => record.toObject() as T); } finally { await session.close(); } } /** * Health check for monitoring */ public async checkHealth(): Promise<{ status: string; mode: string; latency?: number }> { if (!this.driver) { return { status: 'offline', mode: 'unknown' }; } const start = Date.now(); try { await this.driver.verifyConnectivity(); const latency = Date.now() - start; const mode = process.env.NEO4J_URI?.includes('neo4j.io') ? 'cloud' : 'local'; return { status: 'online', mode, latency }; } catch (e) { return { status: 'error', mode: 'unknown' }; } } /** * Optimized Graph Statistics using System Counts (O(1) complexity) * Prevents MemoryPoolOutOfMemoryError on large graphs. * * OLD HEAVY QUERY: MATCH (n) RETURN count(n)... (DO NOT USE) * NEW LIGHTWEIGHT QUERY: Uses Neo4j internal store counts which are instant and use 0 memory. */ public async getGraphStats(): Promise<{ nodes: number; relationships: number; status: string; mode: string }> { if (!this.driver) { return { nodes: 0, relationships: 0, status: 'offline', mode: 'unknown' }; } const session = this.driver.session(); const mode = process.env.NEO4J_URI?.includes('neo4j.io') ? 'cloud' : 'local'; try { // Try APOC first (fastest, most efficient) const result = await session.run(` CALL apoc.meta.stats() YIELD nodeCount, relCount, labels RETURN nodeCount, relCount, labels `).catch(async () => { // Fallback if APOC is not installed: Use count store (still O(1)) // These queries use Neo4j's internal count stores, not full scans console.log('📊 [Neo4j] APOC not available, using count store fallback'); return await session.run(` CALL db.stats.retrieve('GRAPH COUNTS') YIELD data RETURN data `).catch(async () => { // Final fallback: Separate lightweight count queries console.log('📊 [Neo4j] Using basic count queries'); const nodeResult = await session.run('MATCH (n) RETURN count(n) as nodeCount LIMIT 1'); const relResult = await session.run('MATCH ()-[r]->() RETURN count(r) as relCount LIMIT 1'); return { records: [{ get: (key: string) => { if (key === 'nodeCount') return { toNumber: () => nodeResult.records[0]?.get('nodeCount')?.toNumber?.() || 0 }; if (key === 'relCount') return { toNumber: () => relResult.records[0]?.get('relCount')?.toNumber?.() || 0 }; return null; }, has: (key: string) => key === 'nodeCount' || key === 'relCount' }] }; }); }); if (result.records.length > 0) { const record = result.records[0]; // Handle different return structures depending on query used let nodes = 0; let relationships = 0; if (record.has('nodeCount')) { const nodeVal = record.get('nodeCount'); nodes = typeof nodeVal?.toNumber === 'function' ? nodeVal.toNumber() : (nodeVal || 0); } if (record.has('relCount')) { const relVal = record.get('relCount'); relationships = typeof relVal?.toNumber === 'function' ? relVal.toNumber() : (relVal || 0); } if (record.has('data')) { // Handle db.stats.retrieve format const data = record.get('data'); nodes = data?.nodes || 0; relationships = data?.relationships || 0; } return { nodes, relationships, status: 'online', mode }; } return { nodes: 0, relationships: 0, status: 'online', mode }; } catch (error) { console.error('📊 [Neo4j] getGraphStats error:', error); // Let Self-Healing handle the error reporting throw error; } finally { await session.close(); } } /** * Graceful shutdown */ public async disconnect(): Promise { if (this.driver) { await this.driver.close(); this.driver = null; console.log('🔌 Neural Graph connection closed.'); } } /** * Check if connected */ public isConnected(): boolean { return this.driver !== null; } } // Singleton instance export const neo4jService = new Neo4jService();