Kraft102's picture
Update backend source
34367da verified
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://<id>.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<void> {
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<T = any>(cypher: string, params: Record<string, any> = {}): Promise<T[]> {
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<T = any>(cypher: string, params: Record<string, any> = {}): Promise<T[]> {
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<void> {
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();