UNPKG

@codai/cbd

Version:

Codai Better Database - High-Performance Vector Memory System with HPKV-inspired architecture and MCP server

1,107 lines 43.5 kB
/** * CBD Client - CND Compatibility Layer with Full METU Support * * Provides CND-compatible interface using CBD Universal Service backend * Maintains backward compatibility while leveraging CBD's multi-paradigm capabilities * Includes full METU device management, conversation tracking, and message handling */ import { EventEmitter } from 'events'; export class CBDClient extends EventEmitter { baseUrl; connected = false; healthCheckInterval; config; currentDevices = new Map(); currentConversations = new Map(); isCleaningUp = false; constructor(config = {}) { super(); this.config = config; const host = config.cbd?.host || process.env.CBD_HOST || 'localhost'; const port = config.cbd?.port || parseInt(process.env.CBD_PORT || '4180'); this.baseUrl = `http://${host}:${port}`; // Set up event emitter this.setMaxListeners(50); // Initialize schema on connection this.on('connected', () => this.initializeSchema()); } /** * Connect to CBD Universal Service * Replaces CND connect() method */ async connect() { try { // Test connection to CBD Universal Service const response = await fetch(`${this.baseUrl}/health`); if (!response.ok) { throw new Error(`CBD Universal Service not available: ${response.status}`); } const health = await response.json(); console.log(`✅ Connected to CBD Universal Service: ${health.service || 'CBD'}`); this.connected = true; // Emit connected event this.emit('connected'); // Start health monitoring this.startHealthMonitoring(); } catch (error) { console.error('❌ Failed to connect to CBD Universal Service:', error); this.emit('error', error); throw new Error(`CBD connection failed: ${error}`); } } /** * Initialize database schema for METU compatibility */ async initializeSchema() { try { // Initialize METU device schema await this.sql().query(` CREATE TABLE IF NOT EXISTS metu_devices ( id VARCHAR(255) PRIMARY KEY, name VARCHAR(255) NOT NULL, type VARCHAR(100) NOT NULL, status VARCHAR(50) DEFAULT 'inactive', last_seen DATETIME DEFAULT CURRENT_TIMESTAMP, metadata TEXT, version VARCHAR(50), location VARCHAR(255), capabilities TEXT, configuration TEXT, health_score INTEGER DEFAULT 100, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) `); // Initialize METU conversation schema await this.sql().query(` CREATE TABLE IF NOT EXISTS metu_conversations ( id VARCHAR(255) PRIMARY KEY, device_id VARCHAR(255) NOT NULL, title VARCHAR(255), status VARCHAR(50) DEFAULT 'active', participants TEXT, metadata TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, last_activity DATETIME DEFAULT CURRENT_TIMESTAMP, message_count INTEGER DEFAULT 0, FOREIGN KEY (device_id) REFERENCES metu_devices(id) ON DELETE CASCADE ) `); // Initialize METU message schema await this.sql().query(` CREATE TABLE IF NOT EXISTS metu_messages ( id VARCHAR(255) PRIMARY KEY, conversation_id VARCHAR(255) NOT NULL, device_id VARCHAR(255) NOT NULL, content TEXT NOT NULL, type VARCHAR(50) DEFAULT 'text', sender VARCHAR(255), metadata TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, processed BOOLEAN DEFAULT FALSE, response_time INTEGER, FOREIGN KEY (conversation_id) REFERENCES metu_conversations(id) ON DELETE CASCADE, FOREIGN KEY (device_id) REFERENCES metu_devices(id) ON DELETE CASCADE ) `); console.log('✅ METU schema initialized successfully'); this.emit('schema_initialized'); } catch (error) { console.error('❌ Failed to initialize METU schema:', error); this.emit('error', error); } } /** * Start health monitoring for the CBD connection */ startHealthMonitoring() { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); } this.healthCheckInterval = setInterval(async () => { try { const health = await this.getHealthStatus(); this.emit('health_check', health); if (health.status !== 'healthy' && health.status !== 'ok') { console.warn('⚠️ CBD health check warning:', health); this.emit('health_warning', health); } } catch (error) { console.error('❌ Health check failed:', error); this.emit('health_error', error); } }, 30000); // Check every 30 seconds } // ============================================== // METU DEVICE MANAGEMENT METHODS // ============================================== /** * Create a new METU device */ async createDevice(device) { const deviceId = `metu_device_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const deviceData = { ...device, id: deviceId, createdAt: new Date(), updatedAt: new Date(), lastSeen: device.lastSeen || new Date(), healthScore: device.healthScore || 100 }; try { await this.sql().query(`INSERT INTO metu_devices (id, name, type, status, last_seen, metadata, version, location, capabilities, configuration, health_score, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ deviceData.id, deviceData.name, deviceData.type, deviceData.status, deviceData.lastSeen?.toISOString(), JSON.stringify(deviceData.metadata || {}), deviceData.version, deviceData.location, JSON.stringify(deviceData.capabilities || []), JSON.stringify(deviceData.configuration || {}), deviceData.healthScore, deviceData.createdAt?.toISOString(), deviceData.updatedAt?.toISOString() ]); this.currentDevices.set(deviceId, deviceData); this.emit('device_created', deviceData); console.log(`✅ METU device created: ${deviceData.name} (${deviceId})`); return deviceId; } catch (error) { console.error('❌ Failed to create METU device:', error); throw new Error(`Device creation failed: ${error}`); } } /** * Get METU device by ID */ async getDevice(deviceId) { try { const result = await this.sql().query('SELECT * FROM metu_devices WHERE id = ?', [deviceId]); if (!result.data || result.data.length === 0) { return null; } const deviceData = this.parseDeviceFromDB(result.data[0]); this.currentDevices.set(deviceId, deviceData); return deviceData; } catch (error) { console.error(`❌ Failed to get device ${deviceId}:`, error); return null; } } /** * Get all METU devices */ async getAllDevices() { try { const result = await this.sql().query('SELECT * FROM metu_devices ORDER BY created_at DESC'); if (!result.data) { return []; } const devices = result.data.map(row => this.parseDeviceFromDB(row)); // Update current devices cache devices.forEach(device => { if (device.id) { this.currentDevices.set(device.id, device); } }); return devices; } catch (error) { console.error('❌ Failed to get all devices:', error); return []; } } /** * Update METU device */ async updateDevice(deviceId, updates) { try { const currentDevice = await this.getDevice(deviceId); if (!currentDevice) { throw new Error(`Device ${deviceId} not found`); } const updatedDevice = { ...currentDevice, ...updates, id: deviceId, updatedAt: new Date() }; await this.sql().query(`UPDATE metu_devices SET name = ?, type = ?, status = ?, last_seen = ?, metadata = ?, version = ?, location = ?, capabilities = ?, configuration = ?, health_score = ?, updated_at = ? WHERE id = ?`, [ updatedDevice.name, updatedDevice.type, updatedDevice.status, updatedDevice.lastSeen?.toISOString(), JSON.stringify(updatedDevice.metadata || {}), updatedDevice.version, updatedDevice.location, JSON.stringify(updatedDevice.capabilities || []), JSON.stringify(updatedDevice.configuration || {}), updatedDevice.healthScore, updatedDevice.updatedAt?.toISOString(), deviceId ]); this.currentDevices.set(deviceId, updatedDevice); this.emit('device_updated', updatedDevice); console.log(`✅ METU device updated: ${updatedDevice.name} (${deviceId})`); return true; } catch (error) { console.error(`❌ Failed to update device ${deviceId}:`, error); throw new Error(`Device update failed: ${error}`); } } /** * Delete METU device */ async deleteDevice(deviceId) { try { const device = await this.getDevice(deviceId); if (!device) { console.warn(`Device ${deviceId} not found for deletion`); return false; } await this.sql().query('DELETE FROM metu_devices WHERE id = ?', [deviceId]); this.currentDevices.delete(deviceId); this.emit('device_deleted', { id: deviceId, device }); console.log(`✅ METU device deleted: ${device.name} (${deviceId})`); return true; } catch (error) { console.error(`❌ Failed to delete device ${deviceId}:`, error); throw new Error(`Device deletion failed: ${error}`); } } // ============================================== // METU CONVERSATION MANAGEMENT METHODS // ============================================== /** * Create a new conversation for a device */ async createConversation(deviceId, title, metadata) { const conversationId = `metu_conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const conversationData = { id: conversationId, deviceId, title: title || `Conversation with ${deviceId}`, status: 'active', metadata: metadata || {}, createdAt: new Date(), updatedAt: new Date(), lastActivity: new Date(), messageCount: 0 }; try { await this.sql().query(`INSERT INTO metu_conversations (id, device_id, title, status, participants, metadata, created_at, updated_at, last_activity, message_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ conversationData.id, conversationData.deviceId, conversationData.title, conversationData.status, JSON.stringify(conversationData.participants || []), JSON.stringify(conversationData.metadata), conversationData.createdAt?.toISOString(), conversationData.updatedAt?.toISOString(), conversationData.lastActivity?.toISOString(), conversationData.messageCount ]); this.currentConversations.set(conversationId, conversationData); this.emit('conversation_created', conversationData); console.log(`✅ METU conversation created: ${conversationData.title} (${conversationId})`); return conversationId; } catch (error) { console.error('❌ Failed to create METU conversation:', error); throw new Error(`Conversation creation failed: ${error}`); } } /** * Get conversation by ID */ async getConversation(conversationId) { try { const result = await this.sql().query('SELECT * FROM metu_conversations WHERE id = ?', [conversationId]); if (!result.data || result.data.length === 0) { return null; } const conversationData = this.parseConversationFromDB(result.data[0]); this.currentConversations.set(conversationId, conversationData); return conversationData; } catch (error) { console.error(`❌ Failed to get conversation ${conversationId}:`, error); return null; } } /** * Get all conversations for a device */ async getDeviceConversations(deviceId) { try { const result = await this.sql().query('SELECT * FROM metu_conversations WHERE device_id = ? ORDER BY last_activity DESC', [deviceId]); if (!result.data) { return []; } const conversations = result.data.map(row => this.parseConversationFromDB(row)); // Update cache conversations.forEach(conv => { if (conv.id) { this.currentConversations.set(conv.id, conv); } }); return conversations; } catch (error) { console.error(`❌ Failed to get conversations for device ${deviceId}:`, error); return []; } } /** * Update conversation */ async updateConversation(conversationId, updates) { try { const currentConv = await this.getConversation(conversationId); if (!currentConv) { throw new Error(`Conversation ${conversationId} not found`); } const updatedConv = { ...currentConv, ...updates, id: conversationId, updatedAt: new Date() }; await this.sql().query(`UPDATE metu_conversations SET title = ?, status = ?, participants = ?, metadata = ?, updated_at = ?, last_activity = ?, message_count = ? WHERE id = ?`, [ updatedConv.title, updatedConv.status, JSON.stringify(updatedConv.participants || []), JSON.stringify(updatedConv.metadata || {}), updatedConv.updatedAt?.toISOString(), updatedConv.lastActivity?.toISOString(), updatedConv.messageCount, conversationId ]); this.currentConversations.set(conversationId, updatedConv); this.emit('conversation_updated', updatedConv); return true; } catch (error) { console.error(`❌ Failed to update conversation ${conversationId}:`, error); throw new Error(`Conversation update failed: ${error}`); } } // ============================================== // METU MESSAGE MANAGEMENT METHODS // ============================================== /** * Create a new message in a conversation */ async createMessage(message) { const messageId = `metu_msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const messageData = { ...message, id: messageId, createdAt: new Date(), processed: false }; try { await this.sql().query(`INSERT INTO metu_messages (id, conversation_id, device_id, content, type, sender, metadata, created_at, processed, response_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ messageData.id, messageData.conversationId, messageData.deviceId, messageData.content, messageData.type || 'text', messageData.sender, JSON.stringify(messageData.metadata || {}), messageData.createdAt?.toISOString(), messageData.processed ? 1 : 0, messageData.responseTime ]); // Update conversation message count and last activity await this.sql().query(`UPDATE metu_conversations SET message_count = message_count + 1, last_activity = ?, updated_at = ? WHERE id = ?`, [ messageData.createdAt?.toISOString(), messageData.createdAt?.toISOString(), messageData.conversationId ]); // Update device last seen await this.updateDeviceLastSeen(messageData.deviceId); this.emit('message_created', messageData); console.log(`✅ METU message created in conversation ${messageData.conversationId} (${messageId})`); return messageId; } catch (error) { console.error('❌ Failed to create METU message:', error); throw new Error(`Message creation failed: ${error}`); } } /** * Get message by ID */ async getMessage(messageId) { try { const result = await this.sql().query('SELECT * FROM metu_messages WHERE id = ?', [messageId]); if (!result.data || result.data.length === 0) { return null; } return this.parseMessageFromDB(result.data[0]); } catch (error) { console.error(`❌ Failed to get message ${messageId}:`, error); return null; } } /** * Get all messages in a conversation */ async getConversationMessages(conversationId, limit = 100, offset = 0) { try { const result = await this.sql().query('SELECT * FROM metu_messages WHERE conversation_id = ? ORDER BY created_at ASC LIMIT ? OFFSET ?', [conversationId, limit, offset]); if (!result.data) { return []; } return result.data.map(row => this.parseMessageFromDB(row)); } catch (error) { console.error(`❌ Failed to get messages for conversation ${conversationId}:`, error); return []; } } /** * Get recent messages for a device */ async getDeviceMessages(deviceId, limit = 50) { try { const result = await this.sql().query('SELECT * FROM metu_messages WHERE device_id = ? ORDER BY created_at DESC LIMIT ?', [deviceId, limit]); if (!result.data) { return []; } return result.data.map(row => this.parseMessageFromDB(row)); } catch (error) { console.error(`❌ Failed to get messages for device ${deviceId}:`, error); return []; } } /** * Update message (mark as processed, add response time, etc.) */ async updateMessage(messageId, updates) { try { const current = await this.getMessage(messageId); if (!current) { throw new Error(`Message ${messageId} not found`); } const updated = { ...current, ...updates, id: messageId }; await this.sql().query(`UPDATE metu_messages SET content = ?, type = ?, sender = ?, metadata = ?, processed = ?, response_time = ? WHERE id = ?`, [ updated.content, updated.type, updated.sender, JSON.stringify(updated.metadata || {}), updated.processed ? 1 : 0, updated.responseTime, messageId ]); this.emit('message_updated', updated); return true; } catch (error) { console.error(`❌ Failed to update message ${messageId}:`, error); throw new Error(`Message update failed: ${error}`); } } // ============================================== // UTILITY AND PARSER METHODS // ============================================== /** * Parse device data from database row */ parseDeviceFromDB(row) { return { id: row.id, name: row.name, type: row.type, status: row.status, lastSeen: row.last_seen ? new Date(row.last_seen) : new Date(), metadata: row.metadata ? JSON.parse(row.metadata) : {}, version: row.version, location: row.location, capabilities: row.capabilities ? JSON.parse(row.capabilities) : [], configuration: row.configuration ? JSON.parse(row.configuration) : {}, healthScore: row.health_score || 100, createdAt: row.created_at ? new Date(row.created_at) : new Date(), updatedAt: row.updated_at ? new Date(row.updated_at) : new Date() }; } /** * Parse conversation data from database row */ parseConversationFromDB(row) { return { id: row.id, deviceId: row.device_id, title: row.title, status: row.status, participants: row.participants ? JSON.parse(row.participants) : [], metadata: row.metadata ? JSON.parse(row.metadata) : {}, createdAt: row.created_at ? new Date(row.created_at) : new Date(), updatedAt: row.updated_at ? new Date(row.updated_at) : new Date(), lastActivity: row.last_activity ? new Date(row.last_activity) : new Date(), messageCount: row.message_count || 0 }; } /** * Parse message data from database row */ parseMessageFromDB(row) { return { id: row.id, conversationId: row.conversation_id, deviceId: row.device_id, content: row.content, type: row.type || 'text', sender: row.sender, metadata: row.metadata ? JSON.parse(row.metadata) : {}, createdAt: row.created_at ? new Date(row.created_at) : new Date(), processed: Boolean(row.processed), responseTime: row.response_time }; } /** * Get device statistics */ async getDeviceStats(deviceId) { try { const result = await this.sql().query(` SELECT COUNT(DISTINCT c.id) as conversation_count, COUNT(m.id) as message_count, MAX(m.created_at) as last_message_time, AVG(m.response_time) as avg_response_time FROM metu_devices d LEFT JOIN metu_conversations c ON d.id = c.device_id LEFT JOIN metu_messages m ON c.id = m.conversation_id WHERE d.id = ? GROUP BY d.id `, [deviceId]); return result.data?.[0] || { conversation_count: 0, message_count: 0, last_message_time: null, avg_response_time: null }; } catch (error) { console.error(`❌ Failed to get stats for device ${deviceId}:`, error); return null; } } /** * Cleanup old data (messages older than specified days) */ async cleanup(olderThanDays = 30) { if (this.isCleaningUp) { console.log('⚠️ Cleanup already in progress'); return { deletedMessages: 0, deletedConversations: 0 }; } this.isCleaningUp = true; const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - olderThanDays); try { console.log(`🧹 Starting cleanup of data older than ${olderThanDays} days (before ${cutoffDate.toISOString()})`); // Delete old messages const messageResult = await this.sql().query('DELETE FROM metu_messages WHERE created_at < ?', [cutoffDate.toISOString()]); // Delete conversations with no messages const conversationResult = await this.sql().query(` DELETE FROM metu_conversations WHERE id NOT IN ( SELECT DISTINCT conversation_id FROM metu_messages WHERE conversation_id IS NOT NULL ) `); const deletedMessages = messageResult.affectedRows || 0; const deletedConversations = conversationResult.affectedRows || 0; console.log(`✅ Cleanup completed: ${deletedMessages} messages, ${deletedConversations} conversations deleted`); this.emit('cleanup_completed', { deletedMessages, deletedConversations, cutoffDate }); return { deletedMessages, deletedConversations }; } catch (error) { console.error('❌ Cleanup failed:', error); this.emit('cleanup_error', error); throw new Error(`Cleanup failed: ${error}`); } finally { this.isCleaningUp = false; } } /** * Get comprehensive analytics for METU system */ async getAnalytics() { try { const [deviceStats, conversationStats, messageStats] = await Promise.all([ this.sql().query(` SELECT COUNT(*) as total_devices, COUNT(CASE WHEN status = 'active' THEN 1 END) as active_devices, COUNT(CASE WHEN status = 'inactive' THEN 1 END) as inactive_devices, AVG(health_score) as avg_health_score FROM metu_devices `), this.sql().query(` SELECT COUNT(*) as total_conversations, COUNT(CASE WHEN status = 'active' THEN 1 END) as active_conversations, AVG(message_count) as avg_messages_per_conversation FROM metu_conversations `), this.sql().query(` SELECT COUNT(*) as total_messages, COUNT(CASE WHEN processed = 1 THEN 1 END) as processed_messages, AVG(response_time) as avg_response_time, COUNT(CASE WHEN type = 'error' THEN 1 END) as error_messages FROM metu_messages `) ]); return { devices: deviceStats.data?.[0] || {}, conversations: conversationStats.data?.[0] || {}, messages: messageStats.data?.[0] || {}, timestamp: new Date().toISOString() }; } catch (error) { console.error('❌ Failed to get analytics:', error); return null; } } // ============================================== // SQL INTERFACE AND CORE METHODS // ============================================== /** * SQL interface that maps to CBD document operations * Maintains CND compatibility while using CBD backend */ sql() { return { query: async (sql, params = []) => { if (!this.connected) { throw new Error('CBD client not connected. Call connect() first.'); } try { // Parse SQL to determine operation type const operation = this.parseSQLOperation(sql); switch (operation.type) { case 'CREATE_TABLE': return await this.handleCreateTable(operation, params); case 'INSERT': return await this.handleInsert(operation, params); case 'SELECT': return await this.handleSelect(operation, params); case 'UPDATE': return await this.handleUpdate(operation, params); case 'DELETE': return await this.handleDelete(operation, params); default: console.warn(`Unsupported SQL operation: ${sql}`); return { data: [], affectedRows: 0 }; } } catch (error) { console.error('CBD Query Error:', error); return { error: `Query failed: ${error}` }; } } }; } /** * Parse SQL to determine operation and extract table/collection info */ parseSQLOperation(sql) { const normalizedSQL = sql.trim().toUpperCase(); if (normalizedSQL.startsWith('CREATE TABLE')) { const tableMatch = sql.match(/CREATE TABLE (?:IF NOT EXISTS )?(\w+)/i); return { type: 'CREATE_TABLE', table: tableMatch?.[1] || 'unknown', sql: sql }; } if (normalizedSQL.startsWith('INSERT')) { const tableMatch = sql.match(/INSERT INTO (\w+)/i); return { type: 'INSERT', table: tableMatch?.[1] || 'unknown', sql: sql }; } if (normalizedSQL.startsWith('SELECT')) { const tableMatch = sql.match(/FROM (\w+)/i); return { type: 'SELECT', table: tableMatch?.[1] || 'unknown', sql: sql }; } if (normalizedSQL.startsWith('UPDATE')) { const tableMatch = sql.match(/UPDATE (\w+)/i); return { type: 'UPDATE', table: tableMatch?.[1] || 'unknown', sql: sql }; } if (normalizedSQL.startsWith('DELETE')) { const tableMatch = sql.match(/FROM (\w+)/i); return { type: 'DELETE', table: tableMatch?.[1] || 'unknown', sql: sql }; } return { type: 'UNKNOWN', sql: sql }; } /** * Handle CREATE TABLE by creating a CBD document collection */ async handleCreateTable(operation, _params) { // For CREATE TABLE, we just acknowledge it since CBD collections are created on-demand console.log(`📝 CBD: Table '${operation.table}' schema noted (collections created on-demand)`); return { data: [], affectedRows: 0 }; } /** * Handle INSERT by adding document to CBD collection */ async handleInsert(operation, params) { try { // Extract values from INSERT statement and params const insertData = this.extractInsertData(operation.sql, params); const response = await fetch(`${this.baseUrl}/document/`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ collection: operation.table, document: insertData }) }); if (!response.ok) { throw new Error(`Insert failed: ${response.status}`); } const result = await response.json(); return { data: [{ insertId: result.id || 1 }], insertId: result.id || 1, affectedRows: 1 }; } catch (error) { throw new Error(`CBD Insert Error: ${error}`); } } /** * Handle SELECT by querying CBD document collection */ async handleSelect(operation, params) { try { // Enhanced SELECT handling with WHERE clause support const whereClause = this.extractWhereClause(operation.sql, params); let url = `${this.baseUrl}/document/?collection=${operation.table}`; // Add query parameters if WHERE clause exists if (whereClause.field && whereClause.value) { url += `&${whereClause.field}=${encodeURIComponent(whereClause.value)}`; } const response = await fetch(url); if (!response.ok) { throw new Error(`Select failed: ${response.status}`); } const result = await response.json(); let documents = result.documents || []; // Apply additional filtering for complex WHERE clauses if (whereClause.operator && whereClause.operator !== '=') { documents = this.filterDocuments(documents, whereClause); } // Apply LIMIT and OFFSET const limitOffset = this.extractLimitOffset(operation.sql); if (limitOffset.limit) { const start = limitOffset.offset || 0; const end = start + limitOffset.limit; documents = documents.slice(start, end); } return { data: documents, rows: documents }; } catch (error) { throw new Error(`CBD Select Error: ${error}`); } } /** * Handle UPDATE by updating CBD document */ async handleUpdate(operation, params) { try { const updateData = this.extractUpdateData(operation.sql, params); const whereClause = this.extractWhereClause(operation.sql, params); // First, find documents to update const selectResult = await this.handleSelect({ type: 'SELECT', table: operation.table, sql: `SELECT * FROM ${operation.table} WHERE ${whereClause.field} = ?` }, [whereClause.value]); let affectedRows = 0; if (selectResult.data && selectResult.data.length > 0) { for (const doc of selectResult.data) { const updatedDoc = { ...doc, ...updateData }; const response = await fetch(`${this.baseUrl}/document/${doc.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ collection: operation.table, document: updatedDoc }) }); if (response.ok) { affectedRows++; } } } return { data: [], affectedRows }; } catch (error) { throw new Error(`CBD Update Error: ${error}`); } } /** * Handle DELETE by removing CBD document */ async handleDelete(operation, params) { try { const whereClause = this.extractWhereClause(operation.sql, params); // First, find documents to delete const selectResult = await this.handleSelect({ type: 'SELECT', table: operation.table, sql: `SELECT * FROM ${operation.table} WHERE ${whereClause.field} = ?` }, [whereClause.value]); let affectedRows = 0; if (selectResult.data && selectResult.data.length > 0) { for (const doc of selectResult.data) { const response = await fetch(`${this.baseUrl}/document/${doc.id}`, { method: 'DELETE' }); if (response.ok) { affectedRows++; } } } return { data: [], affectedRows }; } catch (error) { throw new Error(`CBD Delete Error: ${error}`); } } /** * Update device last seen timestamp */ async updateDeviceLastSeen(deviceId) { try { const now = new Date(); await this.sql().query("UPDATE metu_devices SET last_seen = ?, updated_at = ? WHERE id = ?", [now.toISOString(), now.toISOString(), deviceId]); // Update cache const cachedDevice = this.currentDevices.get(deviceId); if (cachedDevice) { cachedDevice.lastSeen = now; cachedDevice.updatedAt = now; this.emit("device_activity", { deviceId, lastSeen: now }); } } catch (error) { console.error(`❌ Failed to update last seen for device ${deviceId}:`, error); } } /** * Extract insert data from SQL and parameters */ extractInsertData(sql, params) { // Enhanced extraction for METU-specific data const document = { id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, created_at: new Date().toISOString(), sql_source: sql }; // Try to parse column names from INSERT statement const columnsMatch = sql.match(/INSERT INTO \w+\s*\((.*?)\)/i); if (columnsMatch && params.length > 0) { const columns = columnsMatch[1].split(',').map(col => col.trim().replace(/['"]/g, '')); columns.forEach((column, index) => { if (index < params.length) { document[column] = params[index]; } }); } else { // Fallback: map parameters as indexed fields params.forEach((param, index) => { document[`param_${index}`] = param; }); } return document; } /** * Extract WHERE clause from SQL */ extractWhereClause(sql, params) { const whereMatch = sql.match(/WHERE\s+(\w+)\s*(=|!=|<|>|<=|>=|LIKE)\s*\?/i); if (whereMatch && params.length > 0) { return { field: whereMatch[1], operator: whereMatch[2], value: params[0] }; } return {}; } /** * Extract LIMIT and OFFSET from SQL */ extractLimitOffset(sql) { const limitMatch = sql.match(/LIMIT\s+(\d+)(?:\s+OFFSET\s+(\d+))?/i); if (limitMatch) { return { limit: parseInt(limitMatch[1]), offset: limitMatch[2] ? parseInt(limitMatch[2]) : undefined }; } return {}; } /** * Extract UPDATE data from SQL */ extractUpdateData(sql, params) { const setMatch = sql.match(/SET\s+(.*?)\s+WHERE/i); if (setMatch) { const setPairs = setMatch[1].split(','); const updateData = {}; let paramIndex = 0; setPairs.forEach(pair => { const [field] = pair.split('=').map(p => p.trim()); if (paramIndex < params.length) { updateData[field] = params[paramIndex++]; } }); return updateData; } return {}; } /** * Filter documents based on WHERE clause operators */ filterDocuments(documents, whereClause) { if (!whereClause.field || !whereClause.operator) { return documents; } return documents.filter(doc => { const fieldValue = doc[whereClause.field]; const compareValue = whereClause.value; switch (whereClause.operator.toUpperCase()) { case '!=': case '<>': return fieldValue !== compareValue; case '<': return fieldValue < compareValue; case '>': return fieldValue > compareValue; case '<=': return fieldValue <= compareValue; case '>=': return fieldValue >= compareValue; case 'LIKE': return String(fieldValue).includes(String(compareValue).replace(/%/g, '')); default: return fieldValue === compareValue; } }); } /** * Get health status (replaces CND health methods) */ async getHealthStatus() { try { const response = await fetch(`${this.baseUrl}/health`); const health = await response.json(); // Add METU-specific health info const analytics = await this.getAnalytics(); return { ...health, metu: { devices: analytics?.devices || {}, conversations: analytics?.conversations || {}, messages: analytics?.messages || {}, caches: { devices: this.currentDevices.size, conversations: this.currentConversations.size } }, timestamp: new Date().toISOString() }; } catch (error) { return { status: 'unhealthy', error: String(error), timestamp: new Date().toISOString() }; } } } // Backward compatibility alias export const CND = CBDClient; /** * Factory function for easy CBD client creation */ export function createCBDClient(config = {}) { return new CBDClient(config); } /** * Create METU-compatible CBD client (replaces MetuCNDClient) */ export function createMetuCBDClient(config = {}) { return new CBDClient({ ...config, name: "METU-CBD-Client" }); } //# sourceMappingURL=CBDClient.js.map