// packages/backend/src/repositories/connection.repository.ts import { Database, Statement } from 'sqlite3'; // Import new async helpers and the instance getter import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Remove top-level db instance // const db = getDb(); // Define Connection 类型 (可以从 controller 或 types 文件导入,暂时在此定义) // 注意:这里不包含加密字段,因为 Repository 不应处理解密 interface ConnectionBase { id: number; name: string | null; // 允许 name 为 null host: string; port: number; username: string; auth_method: 'password' | 'key'; proxy_id: number | null; created_at: number; updated_at: number; last_connected_at: number | null; } // Type for the result of the JOIN query in findAllConnectionsWithTags and findConnectionByIdWithTags interface ConnectionWithTagsRow extends ConnectionBase { tag_ids_str: string | null; // Raw string from GROUP_CONCAT } export interface ConnectionWithTags extends ConnectionBase { tag_ids: number[]; } // 包含加密字段的完整类型,用于插入/更新 export interface FullConnectionData extends ConnectionBase { encrypted_password?: string | null; encrypted_private_key?: string | null; encrypted_passphrase?: string | null; // Include tag_ids for creation/update convenience if needed, handled separately tag_ids?: number[]; } // Type for the result of the JOIN query in findFullConnectionById // Define a more specific type for the complex row structure interface FullConnectionDbRow extends FullConnectionData { proxy_db_id: number | null; proxy_name: string | null; proxy_type: string | null; proxy_host: string | null; proxy_port: number | null; proxy_username: string | null; proxy_encrypted_password?: string | null; proxy_encrypted_private_key?: string | null; proxy_encrypted_passphrase?: string | null; } /** * 获取所有连接及其标签 */ export const findAllConnectionsWithTags = async (): Promise => { const sql = ` SELECT c.id, c.name, c.host, c.port, c.username, c.auth_method, c.proxy_id, c.created_at, c.updated_at, c.last_connected_at, GROUP_CONCAT(ct.tag_id) as tag_ids_str FROM connections c LEFT JOIN connection_tags ct ON c.id = ct.connection_id GROUP BY c.id ORDER BY c.name ASC`; try { const db = await getDbInstance(); const rows = await allDb(db, sql); // Safely map rows, handling potential null tag_ids_str return rows.map(row => ({ ...row, tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : [] })); } catch (err: any) { console.error('Repository: 查询连接列表时出错:', err.message); throw new Error('获取连接列表失败'); } }; /** * 根据 ID 获取单个连接及其标签 */ export const findConnectionByIdWithTags = async (id: number): Promise => { const sql = ` SELECT c.id, c.name, c.host, c.port, c.username, c.auth_method, c.proxy_id, c.created_at, c.updated_at, c.last_connected_at, GROUP_CONCAT(ct.tag_id) as tag_ids_str FROM connections c LEFT JOIN connection_tags ct ON c.id = ct.connection_id WHERE c.id = ? GROUP BY c.id`; try { const db = await getDbInstance(); const row = await getDbRow(db, sql, [id]); if (row && typeof row.id !== 'undefined') { // Check if a valid row was found return { ...row, tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number).filter(id => !isNaN(id)) : [] }; } else { return null; } } catch (err: any) { console.error(`Repository: 查询连接 ${id} 时出错:`, err.message); throw new Error('获取连接信息失败'); } }; /** * 根据 ID 获取单个连接的完整信息 (包括加密字段和代理信息) */ export const findFullConnectionById = async (id: number): Promise => { const sql = ` SELECT c.*, -- 选择 connections 表所有列 p.id as proxy_db_id, p.name as proxy_name, p.type as proxy_type, p.host as proxy_host, p.port as proxy_port, p.username as proxy_username, p.encrypted_password as proxy_encrypted_password, p.encrypted_private_key as proxy_encrypted_private_key, p.encrypted_passphrase as proxy_encrypted_passphrase FROM connections c LEFT JOIN proxies p ON c.proxy_id = p.id WHERE c.id = ?`; try { const db = await getDbInstance(); const row = await getDbRow(db, sql, [id]); return row || null; } catch (err: any) { console.error(`Repository: 查询连接 ${id} 详细信息时出错:`, err.message); throw new Error('获取连接详细信息失败'); } }; /** * 创建新连接 (不处理标签) */ export const createConnection = async (data: Omit): Promise => { const now = Math.floor(Date.now() / 1000); const sql = ` INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const params = [ data.name ?? null, data.host, data.port, data.username, data.auth_method, data.encrypted_password ?? null, data.encrypted_private_key ?? null, data.encrypted_passphrase ?? null, data.proxy_id ?? null, now, now ]; try { const db = await getDbInstance(); const result = await runDb(db, sql, params); // Ensure lastID is valid before returning if (typeof result.lastID !== 'number' || result.lastID <= 0) { throw new Error('创建连接后未能获取有效的 lastID'); } return result.lastID; } catch (err: any) { console.error('Repository: 插入连接时出错:', err.message); throw new Error('创建连接记录失败'); } }; /** * 更新连接信息 (不处理标签) */ export const updateConnection = async (id: number, data: Partial>): Promise => { const fieldsToUpdate: { [key: string]: any } = { ...data }; const params: any[] = []; delete fieldsToUpdate.id; delete fieldsToUpdate.created_at; delete fieldsToUpdate.last_connected_at; delete fieldsToUpdate.tag_ids; // Tags handled separately fieldsToUpdate.updated_at = Math.floor(Date.now() / 1000); const setClauses = Object.keys(fieldsToUpdate).map(key => `${key} = ?`).join(', '); Object.values(fieldsToUpdate).forEach(value => params.push(value ?? null)); if (!setClauses) { console.warn(`[Repository] updateConnection called for ID ${id} with no fields to update.`); return false; } params.push(id); const sql = `UPDATE connections SET ${setClauses} WHERE id = ?`; try { const db = await getDbInstance(); const result = await runDb(db, sql, params); return result.changes > 0; } catch (err: any) { console.error(`Repository: 更新连接 ${id} 时出错:`, err.message); throw new Error('更新连接记录失败'); } }; /** * 删除连接 */ export const deleteConnection = async (id: number): Promise => { const sql = `DELETE FROM connections WHERE id = ?`; try { const db = await getDbInstance(); // ON DELETE CASCADE in connection_tags and ON DELETE SET NULL for proxy_id handle related data const result = await runDb(db, sql, [id]); return result.changes > 0; } catch (err: any) { console.error(`Repository: 删除连接 ${id} 时出错:`, err.message); throw new Error('删除连接记录失败'); } }; /** * 更新连接的标签关联 (使用事务) * @param connectionId 连接 ID * @param tagIds 新的标签 ID 数组 (空数组表示清除所有标签) */ export const updateConnectionTags = async (connectionId: number, tagIds: number[]): Promise => { const db = await getDbInstance(); // Use a transaction to ensure atomicity try { await runDb(db, 'BEGIN TRANSACTION'); // 1. Delete old associations await runDb(db, `DELETE FROM connection_tags WHERE connection_id = ?`, [connectionId]); // 2. Insert new associations (if any) if (tagIds.length > 0) { const insertSql = `INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`; // Use Promise.all for potentially better performance, though sequential inserts are safer for constraints const insertPromises = tagIds .filter(tagId => typeof tagId === 'number' && tagId > 0) // Basic validation .map(tagId => runDb(db, insertSql, [connectionId, tagId]).catch(err => { // Log warning but don't fail the whole transaction for a single tag insert error (e.g., invalid tag ID) console.warn(`Repository: 更新连接 ${connectionId} 标签时,插入 tag_id ${tagId} 失败: ${err.message}`); })); await Promise.all(insertPromises); } await runDb(db, 'COMMIT'); } catch (err: any) { console.error(`Repository: 更新连接 ${connectionId} 的标签关联时出错:`, err.message); try { await runDb(db, 'ROLLBACK'); // Attempt to rollback on error } catch (rollbackErr: any) { console.error(`Repository: 回滚连接 ${connectionId} 的标签更新事务失败:`, rollbackErr.message); } throw new Error('处理标签关联失败'); // Re-throw original error } }; /** * 批量插入连接(用于导入) * 注意:此函数应在事务中调用 (由调用者负责事务) * Returns an array mapping new connection IDs to their original import data (for tag association) */ export const bulkInsertConnections = async ( db: Database, // Pass the transaction-aware db instance connections: Array & { tag_ids?: number[] }> ): Promise<{ connectionId: number, originalData: any }[]> => { const insertConnSql = `INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const results: { connectionId: number, originalData: any }[] = []; const now = Math.floor(Date.now() / 1000); // Prepare statement outside the loop for efficiency (though sqlite3 might cache implicitly) // Using direct runDb might be simpler here unless performance is critical for (const connData of connections) { const params = [ connData.name ?? null, connData.host, connData.port, connData.username, connData.auth_method, connData.encrypted_password || null, connData.encrypted_private_key || null, connData.encrypted_passphrase || null, connData.proxy_id || null, now, now ]; try { // Use the passed db instance (which should be in a transaction) const connResult = await runDb(db, insertConnSql, params); if (typeof connResult.lastID !== 'number' || connResult.lastID <= 0) { throw new Error(`插入连接 "${connData.name}" 后未能获取有效的 lastID`); } results.push({ connectionId: connResult.lastID, originalData: connData }); } catch (err: any) { // Log error but continue with other connections? Or re-throw to fail the whole batch? console.error(`Repository: 批量插入连接 "${connData.name}" 时出错: ${err.message}`); // Decide on error handling strategy for batch operations throw new Error(`批量插入连接 "${connData.name}" 失败`); // Fail fast for now } } return results; // Tag insertion should be handled separately after connections are inserted, using the returned IDs };