UNPKG

mcp-gdrive-enhanced-markov

Version:

MCP server for Google Drive and Sheets with write capabilities, AI-powered PDF analysis, SQLite document indexing, and advanced directory exploration tools

450 lines (439 loc) 16.1 kB
import Database from 'better-sqlite3'; import * as path from 'path'; import * as fs from 'fs'; class EnhancedDatabaseManager { db = null; dbPath; constructor() { // Use gdrive_db directory in the repository root const dbDir = path.join(process.cwd(), 'gdrive_db'); if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }); } this.dbPath = path.join(dbDir, 'gdrive-index-v2.db'); } ensureConnection() { if (!this.db) { this.db = new Database(this.dbPath); this.db.pragma('journal_mode = WAL'); this.db.pragma('foreign_keys = ON'); this.db.pragma('busy_timeout = 5000'); this.initializeSchema(); } return this.db; } initializeSchema() { const db = this.db; db.exec(` -- Base documents table CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY, drive_id TEXT UNIQUE NOT NULL, drive_path TEXT, name TEXT NOT NULL, mime_type TEXT, size INTEGER, created_time TEXT, modified_time TEXT, indexed_time TEXT DEFAULT CURRENT_TIMESTAMP, ai_model TEXT, ai_processing_time_ms INTEGER, doc_type TEXT CHECK(doc_type IN ('contract', 'invoice', 'proposal', 'project_plan', 'report', 'unknown')), synopsis TEXT, confidence_score REAL CHECK(confidence_score >= 0 AND confidence_score <= 1) ); -- Contracts table CREATE TABLE IF NOT EXISTS contracts ( id INTEGER PRIMARY KEY, document_id INTEGER NOT NULL UNIQUE, contract_number TEXT, contract_type TEXT, effective_date DATE, expiration_date DATE, auto_renewal BOOLEAN, notice_period_days INTEGER, total_value DECIMAL(15,2), currency TEXT, payment_terms TEXT, governing_law TEXT, FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE ); -- Contract parties CREATE TABLE IF NOT EXISTS contract_parties ( contract_id INTEGER, party_name TEXT, party_role TEXT CHECK(party_role IN ('buyer', 'seller', 'vendor', 'client', 'other')), PRIMARY KEY (contract_id, party_name), FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE ); -- Invoices table CREATE TABLE IF NOT EXISTS invoices ( id INTEGER PRIMARY KEY, document_id INTEGER NOT NULL UNIQUE, invoice_number TEXT, invoice_date DATE, due_date DATE, supplier_name TEXT, supplier_address TEXT, customer_name TEXT, customer_address TEXT, subtotal DECIMAL(15,2), tax_amount DECIMAL(15,2), total_amount DECIMAL(15,2), currency TEXT, payment_status TEXT CHECK(payment_status IN ('paid', 'unpaid', 'partial', 'overdue')), payment_method TEXT, FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE ); -- Invoice line items CREATE TABLE IF NOT EXISTS invoice_items ( id INTEGER PRIMARY KEY, invoice_id INTEGER, description TEXT, quantity DECIMAL(10,3), unit_price DECIMAL(15,2), amount DECIMAL(15,2), FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE ); -- Proposals table CREATE TABLE IF NOT EXISTS proposals ( id INTEGER PRIMARY KEY, document_id INTEGER NOT NULL UNIQUE, proposal_number TEXT, client_name TEXT, project_name TEXT, proposal_date DATE, valid_until DATE, total_value DECIMAL(15,2), currency TEXT, timeline_days INTEGER, status TEXT, FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE ); -- Proposal deliverables CREATE TABLE IF NOT EXISTS proposal_deliverables ( id INTEGER PRIMARY KEY, proposal_id INTEGER, description TEXT, timeline TEXT, value DECIMAL(15,2), FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE ); -- Sync state CREATE TABLE IF NOT EXISTS sync_state ( id INTEGER PRIMARY KEY DEFAULT 1, last_sync_time TEXT, drive_change_token TEXT, total_files_indexed INTEGER DEFAULT 0, total_errors INTEGER DEFAULT 0, CHECK (id = 1) ); -- Processing queue CREATE TABLE IF NOT EXISTS processing_queue ( id INTEGER PRIMARY KEY, drive_id TEXT NOT NULL, operation TEXT CHECK(operation IN ('index', 'reindex', 'delete')), priority INTEGER DEFAULT 5, status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'processing', 'completed', 'failed')), retry_count INTEGER DEFAULT 0, error_message TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, processed_at TEXT ); -- Create indexes CREATE INDEX IF NOT EXISTS idx_doc_type ON documents(doc_type); CREATE INDEX IF NOT EXISTS idx_doc_modified ON documents(modified_time); CREATE INDEX IF NOT EXISTS idx_doc_drive_id ON documents(drive_id); CREATE INDEX IF NOT EXISTS idx_queue_status ON processing_queue(status, priority); CREATE INDEX IF NOT EXISTS idx_contract_expiry ON contracts(expiration_date); CREATE INDEX IF NOT EXISTS idx_invoice_due ON invoices(due_date); CREATE INDEX IF NOT EXISTS idx_doc_name ON documents(name); CREATE INDEX IF NOT EXISTS idx_doc_synopsis ON documents(synopsis); -- Initialize sync state if not exists INSERT OR IGNORE INTO sync_state (id) VALUES (1); `); } // Transaction wrapper async transaction(callback) { const db = this.ensureConnection(); return db.transaction(callback)(db); } // Document operations async insertDocument(doc) { const db = this.ensureConnection(); const stmt = db.prepare(` INSERT INTO documents ( drive_id, drive_path, name, mime_type, size, created_time, modified_time, ai_model, ai_processing_time_ms, doc_type, synopsis, confidence_score ) VALUES ( @drive_id, @drive_path, @name, @mime_type, @size, @created_time, @modified_time, @ai_model, @ai_processing_time_ms, @doc_type, @synopsis, @confidence_score ) `); const result = stmt.run(doc); return result.lastInsertRowid; } async upsertDocument(doc) { const db = this.ensureConnection(); const stmt = db.prepare(` INSERT INTO documents ( drive_id, drive_path, name, mime_type, size, created_time, modified_time, ai_model, ai_processing_time_ms, doc_type, synopsis, confidence_score ) VALUES ( @drive_id, @drive_path, @name, @mime_type, @size, @created_time, @modified_time, @ai_model, @ai_processing_time_ms, @doc_type, @synopsis, @confidence_score ) ON CONFLICT(drive_id) DO UPDATE SET drive_path = excluded.drive_path, name = excluded.name, mime_type = excluded.mime_type, size = excluded.size, modified_time = excluded.modified_time, ai_model = excluded.ai_model, ai_processing_time_ms = excluded.ai_processing_time_ms, doc_type = excluded.doc_type, synopsis = excluded.synopsis, confidence_score = excluded.confidence_score, indexed_time = CURRENT_TIMESTAMP RETURNING id `); const result = stmt.get(doc); return result.id; } async getDocument(id) { const db = this.ensureConnection(); const stmt = db.prepare('SELECT * FROM documents WHERE id = ?'); return stmt.get(id) || null; } async getDocumentByDriveId(driveId) { const db = this.ensureConnection(); const stmt = db.prepare('SELECT * FROM documents WHERE drive_id = ?'); return stmt.get(driveId) || null; } // Contract operations async insertContract(docId, contract) { const db = this.ensureConnection(); const stmt = db.prepare(` INSERT INTO contracts ( document_id, contract_number, contract_type, effective_date, expiration_date, auto_renewal, notice_period_days, total_value, currency, payment_terms, governing_law ) VALUES ( @document_id, @contract_number, @contract_type, @effective_date, @expiration_date, @auto_renewal, @notice_period_days, @total_value, @currency, @payment_terms, @governing_law ) `); const result = stmt.run({ document_id: docId, ...contract }); return result.lastInsertRowid; } async insertContractParties(contractId, parties) { const db = this.ensureConnection(); const stmt = db.prepare(` INSERT INTO contract_parties (contract_id, party_name, party_role) VALUES (@contract_id, @party_name, @party_role) `); for (const party of parties) { stmt.run({ contract_id: contractId, ...party }); } } // Invoice operations async insertInvoice(docId, invoice) { const db = this.ensureConnection(); const stmt = db.prepare(` INSERT INTO invoices ( document_id, invoice_number, invoice_date, due_date, supplier_name, supplier_address, customer_name, customer_address, subtotal, tax_amount, total_amount, currency, payment_status, payment_method ) VALUES ( @document_id, @invoice_number, @invoice_date, @due_date, @supplier_name, @supplier_address, @customer_name, @customer_address, @subtotal, @tax_amount, @total_amount, @currency, @payment_status, @payment_method ) `); const result = stmt.run({ document_id: docId, ...invoice }); return result.lastInsertRowid; } async insertInvoiceItems(invoiceId, items) { const db = this.ensureConnection(); const stmt = db.prepare(` INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, amount) VALUES (@invoice_id, @description, @quantity, @unit_price, @amount) `); for (const item of items) { stmt.run({ invoice_id: invoiceId, ...item }); } } // Proposal operations async insertProposal(docId, proposal) { const db = this.ensureConnection(); const stmt = db.prepare(` INSERT INTO proposals ( document_id, proposal_number, client_name, project_name, proposal_date, valid_until, total_value, currency, timeline_days, status ) VALUES ( @document_id, @proposal_number, @client_name, @project_name, @proposal_date, @valid_until, @total_value, @currency, @timeline_days, @status ) `); const result = stmt.run({ document_id: docId, ...proposal }); return result.lastInsertRowid; } // Queue operations async addToProcessingQueue(items) { const db = this.ensureConnection(); const stmt = db.prepare(` INSERT INTO processing_queue (drive_id, operation, priority) VALUES (@drive_id, @operation, @priority) `); const insertMany = db.transaction((items) => { for (const item of items) { stmt.run(item); } }); insertMany(items); } async getNextQueueItem() { const db = this.ensureConnection(); const stmt = db.prepare(` SELECT * FROM processing_queue WHERE status = 'pending' ORDER BY priority DESC, created_at ASC LIMIT 1 `); return stmt.get() || null; } async updateQueueItemStatus(id, status, error) { const db = this.ensureConnection(); const stmt = db.prepare(` UPDATE processing_queue SET status = @status, error_message = @error_message, processed_at = CASE WHEN @status IN ('completed', 'failed') THEN CURRENT_TIMESTAMP ELSE processed_at END WHERE id = @id `); stmt.run({ id, status, error_message: error }); } // Sync state operations async getSyncState() { const db = this.ensureConnection(); const stmt = db.prepare('SELECT * FROM sync_state WHERE id = 1'); return stmt.get(); } async updateSyncState(updates) { const db = this.ensureConnection(); const fields = Object.keys(updates).map(k => `${k} = @${k}`).join(', '); const stmt = db.prepare(`UPDATE sync_state SET ${fields} WHERE id = 1`); stmt.run(updates); } // Advanced queries async searchDocuments(query, filters) { const db = this.ensureConnection(); let sql = ` SELECT d.* FROM documents d WHERE (d.name LIKE @query OR d.synopsis LIKE @query) `; const params = { query: `%${query}%` }; if (filters?.doc_type) { sql += ' AND d.doc_type = @doc_type'; params.doc_type = filters.doc_type; } if (filters?.date_from) { sql += ' AND d.created_time >= @date_from'; params.date_from = filters.date_from; } if (filters?.date_to) { sql += ' AND d.created_time <= @date_to'; params.date_to = filters.date_to; } sql += ' ORDER BY d.modified_time DESC LIMIT 100'; const stmt = db.prepare(sql); return stmt.all(params); } async getExpiringContracts(days) { const db = this.ensureConnection(); const stmt = db.prepare(` SELECT d.*, c.* FROM documents d JOIN contracts c ON c.document_id = d.id WHERE c.expiration_date BETWEEN date('now') AND date('now', '+' || ? || ' days') ORDER BY c.expiration_date ASC `); return stmt.all(days); } async getOverdueInvoices() { const db = this.ensureConnection(); const stmt = db.prepare(` SELECT d.*, i.* FROM documents d JOIN invoices i ON i.document_id = d.id WHERE i.due_date < date('now') AND i.payment_status IN ('unpaid', 'partial') ORDER BY i.due_date ASC `); return stmt.all(); } async getStats() { const db = this.ensureConnection(); const stats = db.prepare(` SELECT COUNT(*) as total_documents, COUNT(CASE WHEN created_time > datetime('now', '-7 days') THEN 1 END) as documents_last_7_days FROM documents `).get(); const byType = db.prepare(` SELECT doc_type, COUNT(*) as count FROM documents GROUP BY doc_type `).all(); const contractValue = db.prepare(` SELECT COALESCE(SUM(total_value), 0) as total FROM contracts `).get(); const unpaidInvoices = db.prepare(` SELECT COALESCE(SUM(total_amount), 0) as total FROM invoices WHERE payment_status IN ('unpaid', 'partial') `).get(); return { total_documents: stats.total_documents, by_type: Object.fromEntries(byType.map(r => [r.doc_type, r.count])), total_contract_value: contractValue.total, unpaid_invoices_value: unpaidInvoices.total, documents_last_7_days: stats.documents_last_7_days }; } async resetDatabase() { const db = this.ensureConnection(); // Drop all tables db.exec(` DROP TABLE IF EXISTS proposal_deliverables; DROP TABLE IF EXISTS proposals; DROP TABLE IF EXISTS invoice_items; DROP TABLE IF EXISTS invoices; DROP TABLE IF EXISTS contract_parties; DROP TABLE IF EXISTS contracts; DROP TABLE IF EXISTS processing_queue; DROP TABLE IF EXISTS sync_state; DROP TABLE IF EXISTS documents; `); // Reinitialize this.initializeSchema(); } close() { if (this.db) { this.db.close(); this.db = null; } } } // Export singleton instance export const dbV2 = new EnhancedDatabaseManager();