UNPKG

arela

Version:

AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.

325 lines 10.6 kB
/** * GraphDB - SQLite interface for storing and querying codebase graph */ import Database from "better-sqlite3"; export class GraphDB { db; dbPath; constructor(dbPath) { this.dbPath = dbPath; this.db = new Database(dbPath); this.db.pragma("journal_mode = WAL"); this.db.pragma("foreign_keys = ON"); this.initSchema(); } /** * Initialize database schema */ initSchema() { // Files table this.db.exec(` CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY, path TEXT UNIQUE NOT NULL, repo TEXT NOT NULL, type TEXT NOT NULL, lines INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_files_repo ON files(repo); CREATE INDEX IF NOT EXISTS idx_files_type ON files(type); `); // Functions table this.db.exec(` CREATE TABLE IF NOT EXISTS functions ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, line_start INTEGER NOT NULL, line_end INTEGER NOT NULL, is_exported BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_functions_file_id ON functions(file_id); CREATE INDEX IF NOT EXISTS idx_functions_name ON functions(name); `); // Imports table this.db.exec(` CREATE TABLE IF NOT EXISTS imports ( id INTEGER PRIMARY KEY, from_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, to_file_id INTEGER REFERENCES files(id) ON DELETE SET NULL, to_module TEXT, import_type TEXT, imported_names TEXT, line_number INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_imports_from ON imports(from_file_id); CREATE INDEX IF NOT EXISTS idx_imports_to ON imports(to_file_id); `); // Function calls table this.db.exec(` CREATE TABLE IF NOT EXISTS function_calls ( id INTEGER PRIMARY KEY, caller_function_id INTEGER NOT NULL REFERENCES functions(id) ON DELETE CASCADE, callee_function_id INTEGER REFERENCES functions(id) ON DELETE SET NULL, callee_name TEXT, line_number INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_function_calls_caller ON function_calls(caller_function_id); CREATE INDEX IF NOT EXISTS idx_function_calls_callee ON function_calls(callee_function_id); `); // API endpoints table this.db.exec(` CREATE TABLE IF NOT EXISTS api_endpoints ( id INTEGER PRIMARY KEY, method TEXT NOT NULL, path TEXT NOT NULL, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, function_id INTEGER REFERENCES functions(id) ON DELETE SET NULL, line_number INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_api_endpoints_file ON api_endpoints(file_id); CREATE INDEX IF NOT EXISTS idx_api_endpoints_method_path ON api_endpoints(method, path); `); // API calls table this.db.exec(` CREATE TABLE IF NOT EXISTS api_calls ( id INTEGER PRIMARY KEY, method TEXT NOT NULL, url TEXT NOT NULL, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, line_number INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_api_calls_file ON api_calls(file_id); CREATE INDEX IF NOT EXISTS idx_api_calls_method_url ON api_calls(method, url); `); // Metadata table for tracking last ingest this.db.exec(` CREATE TABLE IF NOT EXISTS metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); `); } /** * Add a file to the database */ addFile(file) { const stmt = this.db.prepare(` INSERT INTO files (path, repo, type, lines) VALUES (?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET type = excluded.type, lines = excluded.lines, updated_at = CURRENT_TIMESTAMP RETURNING id `); const result = stmt.get(file.path, file.repoPath, file.type, file.lines); return result.id; } /** * Add a function to the database */ addFunction(fileId, func) { const stmt = this.db.prepare(` INSERT INTO functions (name, file_id, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?) `); const result = stmt.run(func.name, fileId, func.lineStart, func.lineEnd, func.isExported ? 1 : 0); return result.lastInsertRowid; } /** * Add an import relationship */ addImport(fromFileId, toFileId, toModule, importType, importedNames, line) { const stmt = this.db.prepare(` INSERT INTO imports (from_file_id, to_file_id, to_module, import_type, imported_names, line_number) VALUES (?, ?, ?, ?, ?, ?) `); stmt.run(fromFileId, toFileId, toModule, importType, JSON.stringify(importedNames), line); } /** * Add a function call relationship */ addFunctionCall(callerFunctionId, calleeFunctionId, calleeName, line) { const stmt = this.db.prepare(` INSERT INTO function_calls (caller_function_id, callee_function_id, callee_name, line_number) VALUES (?, ?, ?, ?) `); stmt.run(callerFunctionId, calleeFunctionId, calleeName, line); } /** * Add an API endpoint */ addApiEndpoint(endpoint) { const stmt = this.db.prepare(` INSERT INTO api_endpoints (method, path, file_id, function_id, line_number) VALUES (?, ?, ?, ?, ?) `); stmt.run(endpoint.method, endpoint.path, endpoint.fileId, endpoint.functionId, endpoint.line); } /** * Add an API call */ addApiCall(fileId, call) { const stmt = this.db.prepare(` INSERT INTO api_calls (method, url, file_id, line_number) VALUES (?, ?, ?, ?) `); stmt.run(call.method, call.url, fileId, call.line); } /** * Get file ID by path */ getFileId(filePath) { const stmt = this.db.prepare('SELECT id FROM files WHERE path = ?'); const result = stmt.get(filePath); return result?.id ?? null; } /** * Get function ID by name and file ID */ getFunctionId(fileId, functionName) { const stmt = this.db.prepare('SELECT id FROM functions WHERE file_id = ? AND name = ?'); const result = stmt.get(fileId, functionName); return result?.id ?? null; } /** * Query the database */ query(sql, params = []) { const stmt = this.db.prepare(sql); return stmt.all(...params); } /** * Execute raw SQL */ exec(sql) { this.db.exec(sql); } /** * Begin transaction */ beginTransaction() { this.db.exec('BEGIN TRANSACTION'); } /** * Commit transaction */ commit() { this.db.exec('COMMIT'); } /** * Rollback transaction */ rollback() { this.db.exec('ROLLBACK'); } /** * Get summary statistics */ getSummary() { const stats = { filesCount: this.db.prepare('SELECT COUNT(*) as count FROM files').get().count, functionsCount: this.db.prepare('SELECT COUNT(*) as count FROM functions').get().count, importsCount: this.db.prepare('SELECT COUNT(*) as count FROM imports').get().count, functionCallsCount: this.db.prepare('SELECT COUNT(*) as count FROM function_calls').get().count, apiEndpointsCount: this.db.prepare('SELECT COUNT(*) as count FROM api_endpoints').get().count, apiCallsCount: this.db.prepare('SELECT COUNT(*) as count FROM api_calls').get().count, }; return stats; } /** * Clear all data (for refresh) */ clear() { this.db.exec(` DELETE FROM function_calls; DELETE FROM api_calls; DELETE FROM api_endpoints; DELETE FROM imports; DELETE FROM functions; DELETE FROM files; `); } /** * Update metadata (e.g., last_ingest_time) */ setMetadata(key, value) { const stmt = this.db.prepare(` INSERT INTO metadata (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP `); stmt.run(key, value); } /** * Get metadata value */ getMetadata(key) { const stmt = this.db.prepare('SELECT value FROM metadata WHERE key = ?'); const result = stmt.get(key); return result?.value ?? null; } /** * Check if graph is stale (> 24 hours old) */ isStale(maxAgeHours = 24) { const lastIngest = this.getMetadata('last_ingest_time'); if (!lastIngest) return true; // Never ingested const lastIngestTime = new Date(lastIngest).getTime(); const now = Date.now(); const ageHours = (now - lastIngestTime) / (1000 * 60 * 60); return ageHours > maxAgeHours; } /** * Get all files */ getAllFiles() { const stmt = this.db.prepare('SELECT id, path, repo, type, lines FROM files'); return stmt.all(); } /** * Get all imports with file details */ getAllImports() { const stmt = this.db.prepare(` SELECT i.id, i.from_file_id, i.to_file_id, i.to_module, ff.path as from_path, tf.path as to_path FROM imports i LEFT JOIN files ff ON i.from_file_id = ff.id LEFT JOIN files tf ON i.to_file_id = tf.id `); const results = stmt.all(); return results.map(r => ({ id: r.id, from_file_id: r.from_file_id, to_file_id: r.to_file_id, to_module: r.to_module, from_file: r.from_path ? { path: r.from_path } : undefined, to_file: r.to_path ? { path: r.to_path } : undefined, })); } /** * Close the database connection */ close() { this.db.close(); } } //# sourceMappingURL=storage.js.map