This commit is contained in:
Baobhan Sith
2025-04-20 15:23:58 +08:00
parent 1160f8a514
commit 77cd9272ba
31 changed files with 2781 additions and 2113 deletions
@@ -1,164 +1,175 @@
import { getDb } from '../database';
// packages/backend/src/repositories/appearance.repository.ts
// Import new async helpers and the instance getter, ensuring getDb is included
import { getDbInstance, runDb, getDb, allDb } from '../database/connection';
import { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types';
import { defaultUiTheme } from '../config/default-themes'; // Assuming default UI theme is here too
import { defaultUiTheme } from '../config/default-themes';
// Import findThemeById from terminal theme repository for validation
import { findThemeById as findTerminalThemeById } from './terminal-theme.repository';
import * as sqlite3 from 'sqlite3'; // Import sqlite3 for Database type hint
// const db = getDb(); // Removed top-level call to avoid circular dependency issues
const TABLE_NAME = 'appearance_settings';
const SETTINGS_ID = 1; // Use a fixed ID for the single row of global settings
// Remove SETTINGS_ID as the table is key-value based
// const SETTINGS_ID = 1;
/**
* SQL语句:创建 appearance_settings 表
*/
export const SQL_CREATE_TABLE = `
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY, -- Fixed ID for the single settings row
custom_ui_theme TEXT,
active_terminal_theme_id INTEGER NULL, -- 修改为 INTEGER NULL
terminal_font_family TEXT,
terminal_font_size INTEGER,
editor_font_size INTEGER, -- 新增:编辑器字体大小
terminal_background_image TEXT,
page_background_image TEXT,
updated_at INTEGER NOT NULL,
FOREIGN KEY(active_terminal_theme_id) REFERENCES terminal_themes(id) -- 添加外键约束
);
`;
// Define the expected row structure from the database (key-value)
interface DbAppearanceSettingsRow {
key: string;
value: string;
created_at: number;
updated_at: number;
}
/**
* 创建 appearance_settings 表 (如果不存在) - 不再自动调用
*/
const createTableIfNotExists = () => {
// This function is no longer called automatically, initialization is handled in database.ts
getDb().run(SQL_CREATE_TABLE, (err) => {
if (err) {
console.error(`创建 ${TABLE_NAME} 表失败:`, err.message);
} else {
console.log(`${TABLE_NAME} 表已存在或已创建。`);
// 确保默认设置行存在 - 这个调用也应该移到 database.ts
// ensureDefaultSettingsExist();
// Helper function to map DB rows (key-value pairs) to AppearanceSettings object
const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): AppearanceSettings => {
const settings: Partial<AppearanceSettings> = {};
let latestUpdatedAt = 0;
for (const row of rows) {
// Update latestUpdatedAt
if (row.updated_at > latestUpdatedAt) {
latestUpdatedAt = row.updated_at;
}
switch (row.key) {
case 'customUiTheme':
settings.customUiTheme = row.value;
break;
case 'activeTerminalThemeId':
// Ensure value is parsed as number or null
const parsedId = parseInt(row.value, 10);
settings.activeTerminalThemeId = isNaN(parsedId) ? null : parsedId;
break;
case 'terminalFontFamily':
settings.terminalFontFamily = row.value;
break;
case 'terminalFontSize':
settings.terminalFontSize = parseInt(row.value, 10);
break;
case 'editorFontSize':
settings.editorFontSize = parseInt(row.value, 10);
break;
case 'terminalBackgroundImage':
settings.terminalBackgroundImage = row.value || undefined; // Use undefined if empty string
break;
case 'pageBackgroundImage':
settings.pageBackgroundImage = row.value || undefined; // Use undefined if empty string
break;
// Add cases for other potential keys if needed
}
}
});
};
// 辅助函数:将数据库行转换为 AppearanceSettings 对象
const mapRowToAppearanceSettings = (row: any): AppearanceSettings => {
if (!row) return getDefaultAppearanceSettings(); // Return default if no row found
// Merge with defaults for any missing keys and add _id and updatedAt
const defaults = getDefaultAppearanceSettings(); // Get defaults
return {
_id: row.id.toString(),
customUiTheme: row.custom_ui_theme,
activeTerminalThemeId: row.active_terminal_theme_id, // 直接返回数字或 null
terminalFontFamily: row.terminal_font_family,
terminalFontSize: row.terminal_font_size,
editorFontSize: row.editor_font_size, // 新增:编辑器字体大小映射
terminalBackgroundImage: row.terminal_background_image,
// terminalBackgroundOpacity: row.terminal_background_opacity, // Removed
pageBackgroundImage: row.page_background_image,
// pageBackgroundOpacity: row.page_background_opacity, // Removed
updatedAt: row.updated_at,
_id: 'global_appearance', // Use a fixed string ID for the conceptual global settings
customUiTheme: settings.customUiTheme ?? defaults.customUiTheme,
activeTerminalThemeId: settings.activeTerminalThemeId ?? defaults.activeTerminalThemeId,
terminalFontFamily: settings.terminalFontFamily ?? defaults.terminalFontFamily,
terminalFontSize: settings.terminalFontSize ?? defaults.terminalFontSize,
editorFontSize: settings.editorFontSize ?? defaults.editorFontSize,
terminalBackgroundImage: settings.terminalBackgroundImage ?? defaults.terminalBackgroundImage,
pageBackgroundImage: settings.pageBackgroundImage ?? defaults.pageBackgroundImage,
updatedAt: latestUpdatedAt || defaults.updatedAt, // Use latest DB timestamp or default
};
};
// 获取默认外观设置
const getDefaultAppearanceSettings = (): AppearanceSettings => {
// TODO: Find the ID of the default preset theme from terminal_themes table later
// For now, leave activeTerminalThemeId null or undefined
// 获取默认外观设置 (Simplified, _id is no longer relevant here)
const getDefaultAppearanceSettings = (): Omit<AppearanceSettings, '_id'> => {
return {
_id: SETTINGS_ID.toString(),
customUiTheme: JSON.stringify(defaultUiTheme), // Use default UI theme
activeTerminalThemeId: null, // 初始应为 null,待 findAndSetDefaultThemeId 设置
terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"', // Default font
customUiTheme: JSON.stringify(defaultUiTheme),
activeTerminalThemeId: null, // Default should be null initially
terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"',
terminalFontSize: 14,
editorFontSize: 14, // 新增:默认编辑器字体大小
editorFontSize: 14,
terminalBackgroundImage: undefined,
// terminalBackgroundOpacity: 1.0, // Removed
pageBackgroundImage: undefined,
// pageBackgroundOpacity: 1.0, // Removed
updatedAt: Date.now(),
updatedAt: Date.now(), // Provide a default timestamp
};
};
/**
* 确保默认设置行存在,并在需要时设置默认激活主题 ID。
* 这个函数应该在数据库初始化时,在预设主题初始化之后调用。
* Ensures default settings exist in the key-value table.
* This function is called during database initialization.
*/
export const ensureDefaultSettingsExist = async () => { // 改为 async 以便内部 await
export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<void> => {
const defaults = getDefaultAppearanceSettings();
const sqlSelect = `SELECT id, active_terminal_theme_id FROM ${TABLE_NAME} WHERE id = ?`; // 同时查询当前 ID
// 将回调函数改为 async
getDb().get(sqlSelect, [SETTINGS_ID], async (err, row) => {
if (err) {
console.error(`检查默认外观设置时出错:`, err.message);
return;
const nowSeconds = Math.floor(Date.now() / 1000);
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO ${TABLE_NAME} (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
// Define default key-value pairs to ensure existence
const defaultEntries: Array<{ key: keyof Omit<AppearanceSettings, '_id' | 'updatedAt'>, value: any }> = [
{ key: 'customUiTheme', value: defaults.customUiTheme },
{ key: 'activeTerminalThemeId', value: null }, // Start with null
{ key: 'terminalFontFamily', value: defaults.terminalFontFamily },
{ key: 'terminalFontSize', value: defaults.terminalFontSize },
{ key: 'editorFontSize', value: defaults.editorFontSize },
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // Use empty string for DB
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // Use empty string for DB
];
try {
for (const entry of defaultEntries) {
// Convert value to string for DB storage, handle null/undefined
let dbValue: string;
if (entry.value === null || entry.value === undefined) {
dbValue = entry.key === 'activeTerminalThemeId' ? 'null' : ''; // Store null specifically for theme ID, empty otherwise
} else if (typeof entry.value === 'object') {
dbValue = JSON.stringify(entry.value);
} else {
dbValue = String(entry.value);
}
// Special handling for activeTerminalThemeId: store null as 'null' string or the number as string
if (entry.key === 'activeTerminalThemeId') {
dbValue = entry.value === null ? 'null' : String(entry.value);
}
await runDb(db, sqlInsertOrIgnore, [entry.key, dbValue, nowSeconds, nowSeconds]);
}
if (!row) {
const sqlInsert = `
INSERT INTO ${TABLE_NAME} (
id, custom_ui_theme, active_terminal_theme_id, terminal_font_family, terminal_font_size, editor_font_size, -- 添加 editor_font_size 列
terminal_background_image, -- terminal_background_opacity, -- Removed
page_background_image, -- page_background_opacity, -- Removed
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) -- 调整占位符数量
`;
getDb().run(sqlInsert, [
SETTINGS_ID,
defaults.customUiTheme,
defaults.activeTerminalThemeId, // Initially null
defaults.terminalFontFamily,
defaults.terminalFontSize,
defaults.editorFontSize, // 添加 editor_font_size 默认值参数
defaults.terminalBackgroundImage,
// defaults.terminalBackgroundOpacity, // Removed
defaults.pageBackgroundImage,
// defaults.pageBackgroundOpacity, // Removed
defaults.updatedAt
], (insertErr) => {
if (insertErr) {
console.error('插入默认外观设置失败:', insertErr.message);
} else {
console.log('默认外观设置已初始化。');
// Now try to find and set the default theme ID
findAndSetDefaultThemeId();
}
});
} else {
// 如果行已存在,直接调用 findAndSetDefaultThemeId 检查并设置默认 ID
await findAndSetDefaultThemeId(); // 使用 await
}
});
console.log('[AppearanceRepo] 默认外观设置键值对检查完成。');
// After ensuring keys exist, try to set the default theme ID if it's currently null
await findAndSetDefaultThemeIdIfNull(db);
} catch (err: any) {
console.error(`[AppearanceRepo] 检查或插入默认外观设置键值对时出错:`, err.message);
throw new Error(`检查或插入默认外观设置失败: ${err.message}`);
}
};
/**
* 查找默认终端主题 ID 并更新外观设置
* Finds the default terminal theme ID and updates the 'activeTerminalThemeId' setting if it's currently null.
* @param db - The active database instance
*/
const findAndSetDefaultThemeId = async () => {
const findAndSetDefaultThemeIdIfNull = async (db: sqlite3.Database): Promise<void> => {
try {
// Find the default theme from the other table
const defaultThemeSql = `SELECT id FROM terminal_themes WHERE is_system_default = 1 LIMIT 1`;
// Explicitly type the row or use type assertion
getDb().get(defaultThemeSql, [], async (err, defaultThemeRow: { id: number } | undefined) => {
if (err) {
console.error("查找默认终端主题 ID 失败:", err.message);
return;
}
// Check the current value of activeTerminalThemeId
const currentSetting = await getDb<{ value: string }>(db, `SELECT value FROM ${TABLE_NAME} WHERE key = ?`, ['activeTerminalThemeId']);
// Proceed only if the setting exists and its value represents null ('null' string)
if (currentSetting && currentSetting.value === 'null') {
// Find the default theme from the terminal_themes table (assuming name 'default' marks the default)
const defaultThemeSql = `SELECT id FROM terminal_themes WHERE name = 'default' AND theme_type = 'preset' LIMIT 1`;
const defaultThemeRow = await getDb<{ id: number }>(db, defaultThemeSql);
if (defaultThemeRow) {
const defaultThemeIdNum = defaultThemeRow.id; // 直接使用数字 ID
// Check current appearance settings
const currentSettings = await getAppearanceSettings();
// Only set the default theme ID if no active theme ID is currently set (i.e., it's null in the DB)
if (currentSettings && currentSettings.activeTerminalThemeId === null) {
console.log(`数据库中未设置激活终端主题,设置为默认数字 ID: ${defaultThemeIdNum}`);
// 更新时传递数字 ID
await updateAppearanceSettings({ activeTerminalThemeId: defaultThemeIdNum });
} else {
console.log(`数据库中已设置激活终端主题数字 ID (${currentSettings?.activeTerminalThemeId}) 或未找到默认主题,跳过设置默认 ID。`);
}
const defaultThemeIdNum = defaultThemeRow.id;
console.log(`[AppearanceRepo] activeTerminalThemeId 为 null,尝试设置为默认主题 ID: ${defaultThemeIdNum}`);
// Update the setting using INSERT OR REPLACE
const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`;
await runDb(db, sqlReplace, ['activeTerminalThemeId', String(defaultThemeIdNum), Math.floor(Date.now() / 1000)]);
} else {
console.warn("未找到系统默认终端主题,无法设置 activeTerminalThemeId。");
console.warn("[AppearanceRepo] 未找到名为 'default' 的预设终端主题,无法设置默认 activeTerminalThemeId。");
}
});
} catch (error) {
console.error("设置默认终端主题 ID 时出错:", error);
} else {
// console.log(`[AppearanceRepo] activeTerminalThemeId 已设置 (${currentSetting?.value}) 或键不存在,跳过设置默认 ID。`);
}
} catch (error: any) {
console.error("[AppearanceRepo] 设置默认终端主题 ID 时出错:", error.message);
// Don't throw here, just log
}
};
@@ -168,72 +179,90 @@ const findAndSetDefaultThemeId = async () => {
* @returns Promise<AppearanceSettings>
*/
export const getAppearanceSettings = async (): Promise<AppearanceSettings> => {
return new Promise((resolve, reject) => {
getDb().get(`SELECT * FROM ${TABLE_NAME} WHERE id = ?`, [SETTINGS_ID], (err, row) => {
if (err) {
console.error('获取外观设置失败:', err.message);
reject(new Error('获取外观设置失败'));
} else {
resolve(mapRowToAppearanceSettings(row));
}
});
});
try {
const db = await getDbInstance();
// Fetch all rows from the key-value table
const rows = await allDb<DbAppearanceSettingsRow>(db, `SELECT key, value, updated_at FROM ${TABLE_NAME}`);
return mapRowsToAppearanceSettings(rows); // Map the key-value pairs to the settings object
} catch (err: any) {
console.error('获取外观设置失败:', err.message);
throw new Error('获取外观设置失败');
}
};
/**
* 更新外观设置
* 更新外观设置 (Public API)
* @param settingsDto 更新的数据
* @returns Promise<boolean> 是否成功更新
*/
export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise<boolean> => {
const now = Date.now();
let sql = `UPDATE ${TABLE_NAME} SET updated_at = ?`;
const params: any[] = [now];
const db = await getDbInstance();
// Perform validation or complex logic if needed before calling internal update
// Example validation (already present in service, but could be here too):
if (settingsDto.activeTerminalThemeId !== undefined && settingsDto.activeTerminalThemeId !== null) {
try {
const themeExists = await findTerminalThemeById(settingsDto.activeTerminalThemeId);
if (!themeExists) {
throw new Error(`指定的终端主题 ID 不存在: ${settingsDto.activeTerminalThemeId}`);
}
} catch (validationError: any) {
console.error(`[AppearanceRepo] 验证主题 ID ${settingsDto.activeTerminalThemeId} 时出错:`, validationError.message);
throw new Error(`验证主题 ID 失败: ${validationError.message}`);
}
}
// ... other validations ...
// Dynamically build the SET part of the query
const updates: string[] = [];
const validDbKeys = ['custom_ui_theme', 'active_terminal_theme_id', 'terminal_font_family', 'terminal_font_size', 'editor_font_size', 'terminal_background_image', 'page_background_image'];
// Iterate over potential keys to update
for (const key of Object.keys(settingsDto) as Array<keyof UpdateAppearanceDto>) {
const dbKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); // Convert camelCase to snake_case
if (validDbKeys.includes(dbKey)) {
const value = settingsDto[key];
// active_terminal_theme_id 应该是数字或 null
if (dbKey === 'active_terminal_theme_id' && typeof value !== 'number' && value !== null) {
console.error(`[AppearanceRepo] 更新 active_terminal_theme_id 时收到无效类型值: ${value} (类型: ${typeof value}),应为数字或 null。跳过此字段。`);
continue; // 跳过无效类型
}
updates.push(`${dbKey} = ?`);
// 直接推入值 (数字或 null)
params.push(value);
}
}
if (updates.length === 0) {
return true; // Nothing to update
}
sql += `, ${updates.join(', ')} WHERE id = ?`;
params.push(SETTINGS_ID);
return new Promise((resolve, reject) => {
// --- 增加详细日志 ---
console.log(`[AppearanceRepo] Executing SQL: ${sql}`);
console.log(`[AppearanceRepo] With Params: ${JSON.stringify(params)}`);
// --- 日志结束 ---
getDb().run(sql, params, function (err) {
if (err) {
console.error('更新外观设置失败:', err.message);
reject(new Error('更新外观设置失败'));
} else {
console.log(`[AppearanceRepo] 更新外观设置成功,影响行数: ${this.changes}`);
resolve(this.changes > 0);
}
});
});
return updateAppearanceSettingsInternal(db, settingsDto);
};
// 初始化时创建表 - Removed: Initialization is now handled in database.ts
// createTableIfNotExists();
/**
* 内部更新外观设置函数 (供内部调用,如初始化)
* @param db - Active database instance
* @param settingsDto - Data to update
* @returns Promise<boolean> - Success status
*/
// Internal function to update settings in the key-value table
const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDto: UpdateAppearanceDto): Promise<boolean> => {
const nowSeconds = Math.floor(Date.now() / 1000);
const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`;
let changesMade = false;
try {
for (const key of Object.keys(settingsDto) as Array<keyof UpdateAppearanceDto>) {
const value = settingsDto[key];
let dbValue: string;
// Convert value to string for DB, handle null/undefined
if (value === null || value === undefined) {
dbValue = key === 'activeTerminalThemeId' ? 'null' : ''; // Store null specifically for theme ID
} else if (typeof value === 'object') {
dbValue = JSON.stringify(value);
} else {
dbValue = String(value);
}
// Special handling for activeTerminalThemeId to store 'null' string or number string
if (key === 'activeTerminalThemeId') {
dbValue = value === null ? 'null' : String(value);
}
// Validation for active_terminal_theme_id type before saving
if (key === 'activeTerminalThemeId' && value !== null && typeof value !== 'number') {
console.error(`[AppearanceRepo] 更新 activeTerminalThemeId 时收到无效类型值: ${value} (类型: ${typeof value}),应为数字或 null。跳过此字段。`);
continue; // Skip this key
}
// Execute INSERT OR REPLACE for each key-value pair
const result = await runDb(db, sqlReplace, [key, dbValue, nowSeconds]);
if (result.changes > 0) {
changesMade = true;
}
}
console.log(`[AppearanceRepo] 更新外观设置完成。是否有更改: ${changesMade}`);
return changesMade; // Return true if any row was inserted or replaced
} catch (err: any) {
console.error('更新外观设置失败:', err.message);
throw new Error('更新外观设置失败');
}
};
@@ -1,13 +1,15 @@
// packages/backend/src/repositories/audit.repository.ts
import { Database } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { AuditLogEntry, AuditLogActionType } from '../types/audit.types';
export class AuditLogRepository {
private db: Database;
// Define the expected row structure from the database if it matches AuditLogEntry
type DbAuditLogRow = AuditLogEntry;
constructor() {
this.db = getDb();
}
export class AuditLogRepository {
// Remove constructor or leave it empty
// constructor() { }
/**
* 添加一条审计日志记录
@@ -21,28 +23,24 @@ export class AuditLogRepository {
if (details) {
try {
detailsString = typeof details === 'string' ? details : JSON.stringify(details);
} catch (error) {
console.error(`[Audit Log] Failed to stringify details for action ${actionType}:`, error);
detailsString = JSON.stringify({ error: 'Failed to stringify details', originalDetails: details });
} catch (error: any) {
console.error(`[Audit Log] Failed to stringify details for action ${actionType}:`, error.message);
detailsString = JSON.stringify({ error: 'Failed to stringify details', originalDetails: String(details) }); // Ensure originalDetails is stringifiable
}
}
const sql = 'INSERT INTO audit_logs (timestamp, action_type, details) VALUES (?, ?, ?)';
const params = [timestamp, actionType, detailsString];
return new Promise((resolve, reject) => {
this.db.run(sql, params, (err) => {
if (err) {
console.error(`[Audit Log] Error adding log entry for action ${actionType}: ${err.message}`);
// 不拒绝 Promise,记录日志失败不应阻止核心操作
// 但可以在这里触发一个 SERVER_ERROR 通知或日志
resolve(); // Or potentially reject if logging is critical
} else {
// console.log(`[Audit Log] Logged action: ${actionType}`); // Optional: verbose logging
resolve();
}
});
});
try {
const db = await getDbInstance();
await runDb(db, sql, params);
// console.log(`[Audit Log] Logged action: ${actionType}`); // Optional: verbose logging
} catch (err: any) {
console.error(`[Audit Log] Error adding log entry for action ${actionType}: ${err.message}`);
// Decide if logging failure should throw an error or just be logged
// throw new Error(`Error adding log entry: ${err.message}`); // Uncomment to make it critical
}
}
/**
@@ -66,21 +64,9 @@ export class AuditLogRepository {
const params: (string | number)[] = [];
const countParams: (string | number)[] = [];
if (actionType) {
whereClauses.push('action_type = ?');
params.push(actionType);
countParams.push(actionType);
}
if (startDate) {
whereClauses.push('timestamp >= ?');
params.push(startDate);
countParams.push(startDate);
}
if (endDate) {
whereClauses.push('timestamp <= ?');
params.push(endDate);
countParams.push(endDate);
}
if (actionType) { whereClauses.push('action_type = ?'); params.push(actionType); countParams.push(actionType); }
if (startDate) { whereClauses.push('timestamp >= ?'); params.push(startDate); countParams.push(startDate); }
if (endDate) { whereClauses.push('timestamp <= ?'); params.push(endDate); countParams.push(endDate); }
if (whereClauses.length > 0) {
const whereSql = ` WHERE ${whereClauses.join(' AND ')}`;
@@ -91,22 +77,22 @@ export class AuditLogRepository {
baseSql += ' ORDER BY timestamp DESC LIMIT ? OFFSET ?';
params.push(limit, offset);
return new Promise((resolve, reject) => {
try {
const db = await getDbInstance();
// First get the total count
this.db.get(countSql, countParams, (err, row: { total: number }) => {
if (err) {
return reject(new Error(`Error counting audit logs: ${err.message}`));
}
const total = row.total;
const countRow = await getDbRow<{ total: number }>(db, countSql, countParams);
const total = countRow?.total ?? 0;
// Then get the paginated logs
this.db.all(baseSql, params, (err, rows: AuditLogEntry[]) => {
if (err) {
return reject(new Error(`Error fetching audit logs: ${err.message}`));
}
resolve({ logs: rows, total });
});
});
});
// Then get the paginated logs
const logs = await allDb<DbAuditLogRow>(db, baseSql, params);
return { logs, total };
} catch (err: any) {
console.error(`Error fetching audit logs:`, err.message);
throw new Error(`Error fetching audit logs: ${err.message}`);
}
}
}
// Export the class (Removed redundant export below as class is already exported)
// export { AuditLogRepository };
@@ -1,4 +1,5 @@
import { getDb } from '../database';
// packages/backend/src/repositories/command-history.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
// 定义命令历史记录的接口
export interface CommandHistoryEntry {
@@ -7,109 +8,95 @@ export interface CommandHistoryEntry {
timestamp: number; // Unix 时间戳 (秒)
}
// Define the expected row structure from the database if it matches CommandHistoryEntry
type DbCommandHistoryRow = CommandHistoryEntry;
/**
* 插入或更新一条命令历史记录。
* 如果命令已存在,则更新其时间戳;否则,插入新记录。
* @param command - 要添加或更新的命令字符串
* @returns 返回插入或更新记录的 ID
*/
export const upsertCommand = (command: string): Promise<number> => {
const db = getDb();
// 使用 INSERT ... ON CONFLICT DO UPDATE 语法 (SQLite 3.24.0+)
// 如果 command 列冲突 (假设我们为 command 列添加了 UNIQUE 约束,或者手动检查)
// 这里我们先不加 UNIQUE 约束,而是先尝试 UPDATE,再尝试 INSERT
export const upsertCommand = async (command: string): Promise<number> => {
const now = Math.floor(Date.now() / 1000); // 获取当前时间戳
const db = await getDbInstance();
return new Promise((resolve, reject) => {
try {
// 1. 尝试更新现有记录的时间戳
const updateSql = `UPDATE command_history SET timestamp = ? WHERE command = ?`;
db.run(updateSql, [now, command], function (updateErr) {
if (updateErr) {
console.error('更新命令历史记录时间戳时出错:', updateErr);
return reject(new Error('无法更新命令历史记录'));
}
const updateResult = await runDb(db, updateSql, [now, command]);
if (this.changes > 0) {
// 更新成功,需要获取被更新记录的 ID
const selectSql = `SELECT id FROM command_history WHERE command = ? ORDER BY timestamp DESC LIMIT 1`;
db.get(selectSql, [command], (selectErr, row: { id: number } | undefined) => {
if (selectErr) {
console.error('获取更新后记录 ID 时出错:', selectErr);
return reject(new Error('无法获取更新后的记录 ID'));
}
if (row) {
resolve(row.id);
} else {
// 理论上不应该发生,因为我们刚更新了它
reject(new Error('更新成功但无法找到记录 ID'));
}
});
if (updateResult.changes > 0) {
// 更新成功,需要获取被更新记录的 ID
const selectSql = `SELECT id FROM command_history WHERE command = ? ORDER BY timestamp DESC LIMIT 1`;
const row = await getDbRow<{ id: number }>(db, selectSql, [command]);
if (row) {
return row.id;
} else {
// 2. 没有记录被更新,说明命令不存在,执行插入
const insertSql = `INSERT INTO command_history (command, timestamp) VALUES (?, ?)`;
db.run(insertSql, [command, now], function (insertErr) {
if (insertErr) {
console.error('插入新命令历史记录时出错:', insertErr);
return reject(new Error('无法插入新命令历史记录'));
}
resolve(this.lastID); // 返回新插入的行 ID
});
// This case should theoretically not happen if update succeeded
throw new Error('更新成功但无法找到记录 ID');
}
});
});
} else {
// 2. 没有记录被更新,说明命令不存在,执行插入
const insertSql = `INSERT INTO command_history (command, timestamp) VALUES (?, ?)`;
const insertResult = await runDb(db, insertSql, [command, now]);
// Ensure lastID is valid before returning
if (typeof insertResult.lastID !== 'number' || insertResult.lastID <= 0) {
throw new Error('插入新命令历史记录后未能获取有效的 lastID');
}
return insertResult.lastID;
}
} catch (err: any) {
console.error('Upsert 命令历史记录时出错:', err.message);
throw new Error('无法更新或插入命令历史记录');
}
};
/**
* 获取所有命令历史记录,按时间戳升序排列(最旧的在前)
* @returns 返回包含所有历史记录条目的数组
*/
export const getAllCommands = (): Promise<CommandHistoryEntry[]> => {
const db = getDb();
export const getAllCommands = async (): Promise<CommandHistoryEntry[]> => {
const sql = `SELECT id, command, timestamp FROM command_history ORDER BY timestamp ASC`;
return new Promise((resolve, reject) => {
db.all(sql, [], (err, rows: CommandHistoryEntry[]) => {
if (err) {
console.error('获取命令历史记录时出错:', err);
return reject(new Error('无法获取命令历史记录'));
}
resolve(rows);
});
});
try {
const db = await getDbInstance();
const rows = await allDb<DbCommandHistoryRow>(db, sql);
return rows;
} catch (err: any) {
console.error('获取命令历史记录时出错:', err.message);
throw new Error('无法获取命令历史记录');
}
};
/**
* 根据 ID 删除指定的命令历史记录
* @param id - 要删除的记录 ID
* @returns 返回删除的行数 (通常是 1 或 0)
* @returns 返回是否成功删除 (true/false)
*/
export const deleteCommandById = (id: number): Promise<number> => {
const db = getDb();
export const deleteCommandById = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM command_history WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [id], function (err) {
if (err) {
console.error('删除命令历史记录时出错:', err);
return reject(new Error('无法删除命令历史记录'));
}
resolve(this.changes); // 返回受影响的行数
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error('删除命令历史记录时出错:', err.message);
throw new Error('无法删除命令历史记录');
}
};
/**
* 清空所有命令历史记录
* @returns 返回删除的行数
*/
export const clearAllCommands = (): Promise<number> => {
const db = getDb();
export const clearAllCommands = async (): Promise<number> => {
const sql = `DELETE FROM command_history`;
return new Promise((resolve, reject) => {
db.run(sql, [], function (err) {
if (err) {
console.error('清空命令历史记录时出错:', err);
return reject(new Error('无法清空命令历史记录'));
}
resolve(this.changes); // 返回受影响的行数
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql);
return result.changes; // Return the number of deleted rows
} catch (err: any) {
console.error('清空命令历史记录时出错:', err.message);
throw new Error('无法清空命令历史记录');
}
};
@@ -1,9 +1,12 @@
// packages/backend/src/repositories/connection.repository.ts
import { Database, Statement } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
const db = getDb();
// Remove top-level db instance
// const db = getDb();
// 定义 Connection 类型 (可以从 controller 或 types 文件导入,暂时在此定义)
// Define Connection 类型 (可以从 controller 或 types 文件导入,暂时在此定义)
// 注意:这里不包含加密字段,因为 Repository 不应处理解密
interface ConnectionBase {
id: number;
@@ -18,15 +21,36 @@ interface ConnectionBase {
last_connected_at: number | null;
}
interface ConnectionWithTags extends ConnectionBase {
// 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 { // <-- Added export
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;
}
@@ -34,162 +58,147 @@ export interface FullConnectionData extends ConnectionBase { // <-- Added export
* 获取所有连接及其标签
*/
export const findAllConnectionsWithTags = async (): Promise<ConnectionWithTags[]> => {
return new Promise((resolve, reject) => {
db.all(
`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`,
(err, rows: any[]) => {
if (err) {
console.error('Repository: 查询连接列表时出错:', err.message);
return reject(new Error('获取连接列表失败'));
}
const processedRows = rows.map(row => ({
...row,
tag_ids: row.tag_ids_str ? row.tag_ids_str.split(',').map(Number) : []
}));
resolve(processedRows);
}
);
});
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<ConnectionWithTagsRow>(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<ConnectionWithTags | null> => {
return new Promise((resolve, reject) => {
db.get(
`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`,
[id],
(err, row: any) => {
if (err) {
console.error(`Repository: 查询连接 ${id} 时出错:`, err.message);
return reject(new Error('获取连接信息失败'));
}
if (row) {
row.tag_ids = row.tag_ids_str ? row.tag_ids_str.split(',').map(Number) : [];
delete row.tag_ids_str;
resolve(row);
} else {
resolve(null);
}
}
);
});
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<ConnectionWithTagsRow>(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 获取单个连接的完整信息 (包括加密字段)
* 用于更新或测试连接等需要完整信息的场景
* 根据 ID 获取单个连接的完整信息 (包括加密字段和代理信息)
*/
export const findFullConnectionById = async (id: number): Promise<any | null> => {
return new Promise((resolve, reject) => {
// 查询连接信息,并 LEFT JOIN 代理信息 (因为测试连接需要)
// 注意:这里返回的结构比较复杂,服务层需要处理
db.get(
`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, -- 包含代理的 key
p.encrypted_passphrase as proxy_encrypted_passphrase -- 包含代理的 passphrase
FROM connections c
LEFT JOIN proxies p ON c.proxy_id = p.id
WHERE c.id = ?`,
[id],
(err, row: any) => {
if (err) {
console.error(`Repository: 查询连接 ${id} 详细信息时出错:`, err.message);
return reject(new Error('获取连接详细信息失败'));
}
resolve(row || null);
}
);
});
export const findFullConnectionById = async (id: number): Promise<FullConnectionDbRow | null> => {
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<FullConnectionDbRow>(db, sql, [id]);
return row || null;
} catch (err: any) {
console.error(`Repository: 查询连接 ${id} 详细信息时出错:`, err.message);
throw new Error('获取连接详细信息失败');
}
};
/**
* 创建新连接
* 创建新连接 (不处理标签)
*/
// Update function signature to accept name as string | null
export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { name: string | null }): Promise<number> => {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(
`INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
stmt.run(
data.name ?? null, // Ensure null is passed if name is null/undefined
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,
function (this: Statement, err: Error | null) {
stmt.finalize(); // 确保 finalize 被调用
if (err) {
console.error('Repository: 插入连接时出错:', err.message);
return reject(new Error('创建连接记录失败'));
}
resolve((this as any).lastID);
}
);
});
export const createConnection = async (data: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at' | 'tag_ids'>): Promise<number> => {
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('创建连接记录失败');
}
};
/**
* 更新连接信息
* 更新连接信息 (不处理标签)
*/
// Update function signature to accept name as string | null | undefined
export const updateConnection = async (id: number, data: Partial<Omit<FullConnectionData, 'id' | 'created_at' | 'last_connected_at'> & { name?: string | null }>): Promise<boolean> => {
export const updateConnection = async (id: number, data: Partial<Omit<FullConnectionData, 'id' | 'created_at' | 'last_connected_at' | 'tag_ids'>>): Promise<boolean> => {
const fieldsToUpdate: { [key: string]: any } = { ...data };
const params: any[] = [];
// 移除 id, created_at, last_connected_at (不应通过此方法更新)
delete fieldsToUpdate.id;
delete fieldsToUpdate.created_at;
delete fieldsToUpdate.last_connected_at;
delete fieldsToUpdate.tag_ids; // Tags handled separately
// 设置 updated_at
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)); // 处理 undefined 为 null
Object.values(fieldsToUpdate).forEach(value => params.push(value ?? null));
if (!setClauses) {
return false; // 没有要更新的字段
console.warn(`[Repository] updateConnection called for ID ${id} with no fields to update.`);
return false;
}
params.push(id); // 添加 WHERE id = ? 的参数
params.push(id);
const sql = `UPDATE connections SET ${setClauses} WHERE id = ?`;
return new Promise((resolve, reject) => {
const stmt = db.prepare(
`UPDATE connections SET ${setClauses} WHERE id = ?`
);
stmt.run(...params, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 更新连接 ${id} 时出错:`, err.message);
return reject(new Error('更新连接记录失败'));
}
resolve((this as any).changes > 0);
});
});
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('更新连接记录失败');
}
};
@@ -197,116 +206,97 @@ export const updateConnection = async (id: number, data: Partial<Omit<FullConnec
* 删除连接
*/
export const deleteConnection = async (id: number): Promise<boolean> => {
return new Promise((resolve, reject) => {
const stmt = db.prepare(
`DELETE FROM connections WHERE id = ?`
);
stmt.run(id, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 删除连接 ${id} 时出错:`, err.message);
return reject(new Error('删除连接记录失败'));
}
resolve((this as any).changes > 0);
});
});
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<void> => {
const deleteStmt = db.prepare(`DELETE FROM connection_tags WHERE connection_id = ?`);
const insertStmt = db.prepare(`INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`);
const db = await getDbInstance();
// Use a transaction to ensure atomicity
try {
await runDb(db, 'BEGIN TRANSACTION');
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION');
try {
// 1. 删除旧关联
deleteStmt.run(connectionId, (err: Error | null) => {
if (err) throw err;
});
deleteStmt.finalize();
// 1. Delete old associations
await runDb(db, `DELETE FROM connection_tags WHERE connection_id = ?`, [connectionId]);
// 2. 插入新关联 (如果 tagIds 不为空)
if (tagIds.length > 0) {
tagIds.forEach((tagId: any) => {
if (typeof tagId === 'number' && tagId > 0) {
insertStmt.run(connectionId, tagId, (err: Error | null) => {
if (err) throw err;
});
} else {
console.warn(`Repository: 更新连接 ${connectionId} 标签时,提供的 tag_id 无效: ${tagId}`);
}
});
}
insertStmt.finalize();
db.run('COMMIT', (commitErr: Error | null) => {
if (commitErr) throw commitErr;
resolve(); // 事务成功
});
} catch (tagError: any) {
console.error(`Repository: 更新连接 ${connectionId} 的标签关联时出错:`, tagError);
db.run('ROLLBACK');
reject(new Error('处理标签关联失败'));
}
});
});
// 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 (connections: Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'>[]): Promise<{ connectionId: number, originalData: any }[]> => {
const insertConnStmt = db.prepare(`INSERT INTO connections (name, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, proxy_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const insertTagStmt = db.prepare(`INSERT INTO connection_tags (connection_id, tag_id) VALUES (?, ?)`);
export const bulkInsertConnections = async (
db: Database, // Pass the transaction-aware db instance
connections: Array<Omit<FullConnectionData, 'id' | 'created_at' | 'updated_at' | 'last_connected_at'> & { 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);
try {
for (const connData of connections) {
const connResult = await new Promise<{ lastID: number }>((resolve, reject) => {
insertConnStmt.run(
connData.name, 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,
function (this: Statement, err: Error | null) {
if (err) return reject(new Error(`插入连接 "${connData.name}" 时出错: ${err.message}`));
resolve({ lastID: (this as any).lastID });
}
);
});
const newConnectionId = connResult.lastID;
results.push({ connectionId: newConnectionId, originalData: connData }); // Store ID and original data for tag association
// Prepare statement outside the loop for efficiency (though sqlite3 might cache implicitly)
// Using direct runDb might be simpler here unless performance is critical
// 处理标签关联 (在同一个事务中)
if (Array.isArray((connData as any).tag_ids) && (connData as any).tag_ids.length > 0) {
for (const tagId of (connData as any).tag_ids) {
if (typeof tagId === 'number' && tagId > 0) {
await new Promise<void>((resolve, reject) => {
insertTagStmt.run(newConnectionId, tagId, (err: Error | null) => {
if (err) {
// 警告但不中断整个导入
console.warn(`Repository: 导入连接 ${connData.name}: 关联标签 ID ${tagId} 失败: ${err.message}`);
}
resolve();
});
});
}
}
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;
} finally {
// Finalize statements after the loop
insertConnStmt.finalize();
insertTagStmt.finalize();
}
return results;
// Tag insertion should be handled separately after connections are inserted, using the returned IDs
};
@@ -1,5 +1,7 @@
// packages/backend/src/repositories/notification.repository.ts
import { Database } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
import { NotificationSetting, RawNotificationSetting, NotificationChannelType, NotificationEvent, NotificationChannelConfig } from '../types/notification.types';
// Helper to parse raw data from DB
@@ -11,85 +13,81 @@ const parseRawSetting = (raw: RawNotificationSetting): NotificationSetting => {
config: JSON.parse(raw.config || '{}'),
enabled_events: JSON.parse(raw.enabled_events || '[]'),
};
} catch (error) {
console.error(`Error parsing notification setting ID ${raw.id}:`, error);
// Return a default/error state or re-throw, depending on desired handling
// For now, return partially parsed with defaults for JSON fields
// Cast to satisfy type checker, but this indicates a parsing error.
} catch (error: any) { // Add type annotation
console.error(`Error parsing notification setting ID ${raw.id}:`, error.message);
return {
...raw,
enabled: Boolean(raw.enabled),
config: {} as NotificationChannelConfig, // Config is invalid due to parsing error
config: {} as NotificationChannelConfig, // Indicate parsing error
enabled_events: [],
};
}
};
export class NotificationSettingsRepository {
private db: Database;
constructor() {
this.db = getDb();
}
// Remove constructor or leave it empty
// constructor() { }
async getAll(): Promise<NotificationSetting[]> {
return new Promise((resolve, reject) => {
this.db.all('SELECT * FROM notification_settings ORDER BY created_at ASC', (err, rows: RawNotificationSetting[]) => {
if (err) {
return reject(new Error(`Error fetching notification settings: ${err.message}`));
}
resolve(rows.map(parseRawSetting));
});
});
try {
const db = await getDbInstance();
const rows = await allDb<RawNotificationSetting>(db, 'SELECT * FROM notification_settings ORDER BY created_at ASC');
return rows.map(parseRawSetting);
} catch (err: any) {
console.error(`Error fetching notification settings:`, err.message);
throw new Error(`Error fetching notification settings: ${err.message}`);
}
}
async getById(id: number): Promise<NotificationSetting | null> {
return new Promise((resolve, reject) => {
this.db.get('SELECT * FROM notification_settings WHERE id = ?', [id], (err, row: RawNotificationSetting) => {
if (err) {
return reject(new Error(`Error fetching notification setting by ID ${id}: ${err.message}`));
}
resolve(row ? parseRawSetting(row) : null);
});
});
try {
const db = await getDbInstance();
const row = await getDbRow<RawNotificationSetting>(db, 'SELECT * FROM notification_settings WHERE id = ?', [id]);
return row ? parseRawSetting(row) : null;
} catch (err: any) {
console.error(`Error fetching notification setting by ID ${id}:`, err.message);
throw new Error(`Error fetching notification setting by ID ${id}: ${err.message}`);
}
}
async getEnabledByEvent(event: NotificationEvent): Promise<NotificationSetting[]> {
return new Promise((resolve, reject) => {
// Note: This query is inefficient as it fetches all enabled settings and filters in code.
// For better performance with many settings, consider normalizing enabled_events
// or using JSON functions if the SQLite version supports them well.
this.db.all('SELECT * FROM notification_settings WHERE enabled = 1', (err, rows: RawNotificationSetting[]) => {
if (err) {
return reject(new Error(`Error fetching enabled notification settings: ${err.message}`));
}
const parsedRows = rows.map(parseRawSetting);
const filteredRows = parsedRows.filter(setting => setting.enabled_events.includes(event));
resolve(filteredRows);
});
});
// Note: Query remains inefficient, consider optimization later if needed.
try {
const db = await getDbInstance();
const rows = await allDb<RawNotificationSetting>(db, 'SELECT * FROM notification_settings WHERE enabled = 1');
const parsedRows = rows.map(parseRawSetting);
const filteredRows = parsedRows.filter(setting => setting.enabled_events.includes(event));
return filteredRows;
} catch (err: any) {
console.error(`Error fetching enabled notification settings:`, err.message);
throw new Error(`Error fetching enabled notification settings: ${err.message}`);
}
}
async create(setting: Omit<NotificationSetting, 'id' | 'created_at' | 'updated_at'>): Promise<number> {
const sql = `
INSERT INTO notification_settings (channel_type, name, enabled, config, enabled_events)
VALUES (?, ?, ?, ?, ?)
`;
INSERT INTO notification_settings (channel_type, name, enabled, config, enabled_events, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
`; // Added created_at, updated_at
const params = [
setting.channel_type,
setting.name,
setting.name ?? '', // Ensure name is not undefined
setting.enabled ? 1 : 0,
JSON.stringify(setting.config || {}),
JSON.stringify(setting.enabled_events || [])
];
return new Promise((resolve, reject) => {
this.db.run(sql, params, function (err) { // Use function() to access this.lastID
if (err) {
return reject(new Error(`Error creating notification setting: ${err.message}`));
}
resolve(this.lastID);
});
});
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(`Error creating notification setting:`, err.message);
throw new Error(`Error creating notification setting: ${err.message}`);
}
}
async update(id: number, setting: Partial<Omit<NotificationSetting, 'id' | 'created_at' | 'updated_at'>>): Promise<boolean> {
@@ -97,29 +95,16 @@ export class NotificationSettingsRepository {
const fields: string[] = [];
const params: (string | number | null)[] = [];
if (setting.channel_type !== undefined) {
fields.push('channel_type = ?');
params.push(setting.channel_type);
}
if (setting.name !== undefined) {
fields.push('name = ?');
params.push(setting.name);
}
if (setting.enabled !== undefined) {
fields.push('enabled = ?');
params.push(setting.enabled ? 1 : 0);
}
if (setting.config !== undefined) {
fields.push('config = ?');
params.push(JSON.stringify(setting.config || {}));
}
if (setting.enabled_events !== undefined) {
fields.push('enabled_events = ?');
params.push(JSON.stringify(setting.enabled_events || []));
}
// Dynamically build SET clauses
if (setting.channel_type !== undefined) { fields.push('channel_type = ?'); params.push(setting.channel_type); }
if (setting.name !== undefined) { fields.push('name = ?'); params.push(setting.name); }
if (setting.enabled !== undefined) { fields.push('enabled = ?'); params.push(setting.enabled ? 1 : 0); }
if (setting.config !== undefined) { fields.push('config = ?'); params.push(JSON.stringify(setting.config || {})); }
if (setting.enabled_events !== undefined) { fields.push('enabled_events = ?'); params.push(JSON.stringify(setting.enabled_events || [])); }
if (fields.length === 0) {
return Promise.resolve(true); // Nothing to update
console.warn(`[NotificationRepo] update called for ID ${id} with no fields to update.`);
return true; // Or false, depending on desired behavior for no-op update
}
fields.push('updated_at = strftime(\'%s\', \'now\')'); // Always update timestamp
@@ -127,25 +112,28 @@ export class NotificationSettingsRepository {
const sql = `UPDATE notification_settings SET ${fields.join(', ')} WHERE id = ?`;
params.push(id);
return new Promise((resolve, reject) => {
this.db.run(sql, params, function (err) { // Use function() to access this.changes
if (err) {
return reject(new Error(`Error updating notification setting ID ${id}: ${err.message}`));
}
resolve(this.changes > 0);
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
return result.changes > 0;
} catch (err: any) {
console.error(`Error updating notification setting ID ${id}:`, err.message);
throw new Error(`Error updating notification setting ID ${id}: ${err.message}`);
}
}
async delete(id: number): Promise<boolean> {
const sql = 'DELETE FROM notification_settings WHERE id = ?';
return new Promise((resolve, reject) => {
this.db.run(sql, [id], function (err) { // Use function() to access this.changes
if (err) {
return reject(new Error(`Error deleting notification setting ID ${id}: ${err.message}`));
}
resolve(this.changes > 0);
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Error deleting notification setting ID ${id}:`, err.message);
throw new Error(`Error deleting notification setting ID ${id}: ${err.message}`);
}
}
}
// Export the class (Removed redundant export below as class is already exported)
// export { NotificationSettingsRepository };
@@ -1,5 +1,7 @@
// packages/backend/src/repositories/passkey.repository.ts
import { Database } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义 Passkey 数据库记录的接口
export interface PasskeyRecord {
@@ -13,20 +15,15 @@ export interface PasskeyRecord {
updated_at: number;
}
export class PasskeyRepository {
private db: Database;
// Define the expected row structure from the database if it matches PasskeyRecord
type DbPasskeyRow = PasskeyRecord;
constructor() {
this.db = getDb();
}
export class PasskeyRepository {
// Remove constructor or leave it empty, db instance will be fetched in each method
// constructor() { }
/**
* 保存新的 Passkey 凭证
* @param credentialId Base64URL 编码的凭证 ID
* @param publicKey Base64URL 编码的公钥
* @param counter 签名计数器
* @param transports 传输方式 (JSON 字符串)
* @param name 用户提供的名称 (可选)
* @returns Promise<number> 新插入记录的 ID
*/
async savePasskey(
@@ -40,136 +37,147 @@ export class PasskeyRepository {
INSERT INTO passkeys (credential_id, public_key, counter, transports, name, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))
`;
return new Promise((resolve, reject) => {
this.db.run(sql, [credentialId, publicKey, counter, transports, name ?? null], function (err) {
if (err) {
console.error('保存 Passkey 时出错:', err.message);
return reject(new Error(`保存 Passkey 时出错: ${err.message}`));
}
resolve(this.lastID);
});
});
const params = [credentialId, publicKey, counter, transports, name ?? null];
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('保存 Passkey 后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('保存 Passkey 时出错:', err.message);
// Handle potential UNIQUE constraint errors on credential_id
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`Credential ID "${credentialId}" 已存在。`);
}
throw new Error(`保存 Passkey 时出错: ${err.message}`);
}
}
/**
* 根据 Credential ID 获取 Passkey 记录
* @param credentialId Base64URL 编码的凭证 ID
* @returns Promise<PasskeyRecord | null> 找到的记录或 null
*/
async getPasskeyByCredentialId(credentialId: string): Promise<PasskeyRecord | null> {
const sql = `SELECT * FROM passkeys WHERE credential_id = ?`;
return new Promise((resolve, reject) => {
this.db.get(sql, [credentialId], (err, row: PasskeyRecord) => {
if (err) {
console.error('按 Credential ID 获取 Passkey 时出错:', err.message);
return reject(new Error(`按 Credential ID 获取 Passkey 时出错: ${err.message}`));
}
resolve(row || null);
});
});
try {
const db = await getDbInstance();
const row = await getDbRow<DbPasskeyRow>(db, sql, [credentialId]);
return row || null;
} catch (err: any) {
console.error('按 Credential ID 获取 Passkey 时出错:', err.message);
throw new Error(`按 Credential ID 获取 Passkey 时出错: ${err.message}`);
}
}
/**
* 获取所有已注册的 Passkey 记录
* @returns Promise<PasskeyRecord[]> 所有记录的数组
* 获取所有已注册的 Passkey 记录 (仅选择必要字段)
* @returns Promise<Partial<PasskeyRecord>[]> 所有记录的部分信息的数组
*/
async getAllPasskeys(): Promise<PasskeyRecord[]> {
const sql = `SELECT id, credential_id, name, transports, created_at FROM passkeys ORDER BY created_at DESC`; // 仅选择必要字段
return new Promise((resolve, reject) => {
this.db.all(sql, [], (err, rows: PasskeyRecord[]) => {
if (err) {
console.error('获取所有 Passkey 时出错:', err.message);
return reject(new Error(`获取所有 Passkey 时出错: ${err.message}`));
}
resolve(rows);
});
});
// Adjust return type based on selected columns
async getAllPasskeys(): Promise<Array<Pick<PasskeyRecord, 'id' | 'credential_id' | 'name' | 'transports' | 'created_at'>>> {
const sql = `SELECT id, credential_id, name, transports, created_at FROM passkeys ORDER BY created_at DESC`;
try {
const db = await getDbInstance();
// Adjust the generic type for allDb to match the selected columns
const rows = await allDb<Pick<PasskeyRecord, 'id' | 'credential_id' | 'name' | 'transports' | 'created_at'>>(db, sql);
return rows;
} catch (err: any) {
console.error('获取所有 Passkey 时出错:', err.message);
throw new Error(`获取所有 Passkey 时出错: ${err.message}`);
}
}
/**
* 更新 Passkey 的签名计数器
* @param credentialId Base64URL 编码的凭证 ID
* @param newCounter 新的计数器值
* @returns Promise<void>
*/
async updatePasskeyCounter(credentialId: string, newCounter: number): Promise<void> {
const sql = `UPDATE passkeys SET counter = ?, updated_at = strftime('%s', 'now') WHERE credential_id = ?`;
return new Promise((resolve, reject) => {
this.db.run(sql, [newCounter, credentialId], function (err) {
if (err) {
console.error('更新 Passkey 计数器时出错:', err.message);
return reject(new Error(`更新 Passkey 计数器时出错: ${err.message}`));
}
if (this.changes === 0) {
return reject(new Error(`未找到 Credential ID 为 ${credentialId} 的 Passkey 进行更新`));
}
resolve();
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [newCounter, credentialId]);
if (result.changes === 0) {
// Consider if this should be an error or just a warning/no-op
console.warn(`未找到 Credential ID 为 ${credentialId} 的 Passkey 进行计数器更新`);
// throw new Error(`未找到 Credential ID 为 ${credentialId} 的 Passkey 进行更新`);
}
} catch (err: any) {
console.error('更新 Passkey 计数器时出错:', err.message);
throw new Error(`更新 Passkey 计数器时出错: ${err.message}`);
}
}
/**
* 根据 ID 删除 Passkey
* @param id Passkey 记录的 ID
* @returns Promise<void>
* @returns Promise<boolean> 是否成功删除
*/
async deletePasskeyById(id: number): Promise<void> {
async deletePasskeyById(id: number): Promise<boolean> {
const sql = `DELETE FROM passkeys WHERE id = ?`;
return new Promise((resolve, reject) => {
this.db.run(sql, [id], function (err) {
if (err) {
console.error('按 ID 删除 Passkey 时出错:', err.message);
return reject(new Error(`ID 删除 Passkey 时出错: ${err.message}`));
}
if (this.changes === 0) {
return reject(new Error(`未找到 ID ${id} 的 Passkey 进行删除`));
}
console.log(`ID 为 ${id} 的 Passkey 已删除。`);
resolve();
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
if (result.changes > 0) {
console.log(`ID ${id} 的 Passkey 已删除。`);
return true;
} else {
console.warn(`尝试删除不存在的 Passkey ID: ${id}`);
return false;
}
} catch (err: any) {
console.error('按 ID 删除 Passkey 时出错:', err.message);
throw new Error(`按 ID 删除 Passkey 时出错: ${err.message}`);
}
}
/**
* 根据 Credential ID 删除 Passkey
* @param credentialId Base64URL 编码的凭证 ID
* @returns Promise<void>
* @returns Promise<boolean> 是否成功删除
*/
async deletePasskeyByCredentialId(credentialId: string): Promise<void> {
async deletePasskeyByCredentialId(credentialId: string): Promise<boolean> {
const sql = `DELETE FROM passkeys WHERE credential_id = ?`;
return new Promise((resolve, reject) => {
this.db.run(sql, [credentialId], function (err) {
if (err) {
console.error('按 Credential ID 删除 Passkey 时出错:', err.message);
return reject(new Error(`Credential ID 删除 Passkey 时出错: ${err.message}`));
}
if (this.changes === 0) {
// It's possible the user tries to delete a non-existent key, maybe not an error?
console.warn(`尝试删除不存在的 Credential ID: ${credentialId}`);
} else {
console.log(`Credential ID 为 ${credentialId} 的 Passkey 已删除。`);
}
resolve();
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [credentialId]);
if (result.changes > 0) {
console.log(`Credential ID ${credentialId} 的 Passkey 已删除。`);
return true;
} else {
console.warn(`尝试删除不存在的 Credential ID: ${credentialId}`);
return false;
}
} catch (err: any) {
console.error('按 Credential ID 删除 Passkey 时出错:', err.message);
throw new Error(`按 Credential ID 删除 Passkey 时出错: ${err.message}`);
}
}
/**
* 根据 credential_id 或 name 前缀模糊查找 Passkey 记录(自动补全)
* @param prefix 前缀字符串
* @returns Promise<PasskeyRecord[]> 匹配的记录数组
*/
async searchPasskeyByPrefix(prefix: string): Promise<PasskeyRecord[]> {
// Adjust return type based on selected columns if not selecting all (*)
async searchPasskeyByPrefix(prefix: string): Promise<DbPasskeyRow[]> {
const sql = `SELECT * FROM passkeys WHERE credential_id LIKE ? OR name LIKE ? ORDER BY created_at DESC`;
const likePrefix = `${prefix}%`;
return new Promise((resolve, reject) => {
this.db.all(sql, [likePrefix, likePrefix], (err, rows: PasskeyRecord[]) => {
if (err) {
console.error('模糊查找 Passkey 时出错:', err.message);
return reject(new Error(`模糊查找 Passkey 时出错: ${err.message}`));
}
resolve(rows);
});
});
try {
const db = await getDbInstance();
const rows = await allDb<DbPasskeyRow>(db, sql, [likePrefix, likePrefix]);
return rows;
} catch (err: any) {
console.error('模糊查找 Passkey 时出错:', err.message);
throw new Error(`模糊查找 Passkey 时出错: ${err.message}`);
}
}
}
// Export an instance or the class itself depending on usage pattern
// If used as a singleton service, export an instance:
// export const passkeyRepository = new PasskeyRepository();
// If instantiated elsewhere (e.g., dependency injection), export the class:
// export { PasskeyRepository };
// For now, let's assume it's used like other repositories (exporting functions/class)
// Exporting the class seems more appropriate given its structure
// Removed redundant export below as the class is already exported with 'export class'
@@ -1,7 +1,10 @@
// packages/backend/src/repositories/proxy.repository.ts
import { Database, Statement } from 'sqlite3';
import { getDb } from '../database';
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
const db = getDb();
// Remove top-level db instance
// const db = getDb();
// 定义 Proxy 类型 (可以共享到 types 文件)
export interface ProxyData {
@@ -19,83 +22,83 @@ export interface ProxyData {
updated_at: number;
}
// Define the expected row structure from the database if it matches ProxyData
type DbProxyRow = ProxyData;
/**
* 根据名称、类型、主机和端口查找代理
*/
export const findProxyByNameTypeHostPort = async (name: string, type: string, host: string, port: number): Promise<{ id: number } | undefined> => {
return new Promise((resolve, reject) => {
db.get(
`SELECT id FROM proxies WHERE name = ? AND type = ? AND host = ? AND port = ?`,
[name, type, host, port],
(err: Error | null, row: { id: number } | undefined) => {
if (err) {
console.error(`Repository: 查找代理时出错 (name=${name}, type=${type}, host=${host}, port=${port}):`, err.message);
return reject(new Error(`查找代理时出错: ${err.message}`));
}
resolve(row);
}
);
});
const sql = `SELECT id FROM proxies WHERE name = ? AND type = ? AND host = ? AND port = ?`;
try {
const db = await getDbInstance();
const row = await getDbRow<{ id: number }>(db, sql, [name, type, host, port]);
return row;
} catch (err: any) {
console.error(`Repository: 查找代理时出错 (name=${name}, type=${type}, host=${host}, port=${port}):`, err.message);
throw new Error(`查找代理时出错: ${err.message}`);
}
};
/**
* 创建新代理
*/
export const createProxy = async (data: Omit<ProxyData, 'id' | 'created_at' | 'updated_at'>): Promise<number> => {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(
`INSERT INTO proxies (name, type, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
stmt.run(
data.name, data.type, data.host, data.port,
data.username || null,
data.auth_method || 'none',
data.encrypted_password || null,
data.encrypted_private_key || null,
data.encrypted_passphrase || null,
now, now,
function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error('Repository: 创建代理时出错:', err.message);
return reject(new Error(`创建代理时出错: ${err.message}`));
}
resolve((this as any).lastID);
}
);
});
const now = Math.floor(Date.now() / 1000);
const sql = `INSERT INTO proxies (name, type, host, port, username, auth_method, encrypted_password, encrypted_private_key, encrypted_passphrase, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
const params = [
data.name, data.type, data.host, data.port,
data.username || null,
data.auth_method || 'none',
data.encrypted_password || null,
data.encrypted_private_key || null,
data.encrypted_passphrase || 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);
// Handle potential UNIQUE constraint errors if needed (e.g., on name)
throw new Error(`创建代理时出错: ${err.message}`);
}
};
/**
* 获取所有代理
*/
export const findAllProxies = async (): Promise<ProxyData[]> => {
return new Promise((resolve, reject) => {
db.all(`SELECT * FROM proxies ORDER BY name ASC`, (err, rows: ProxyData[]) => {
if (err) {
console.error('Repository: 查询代理列表时出错:', err.message);
return reject(new Error('获取代理列表失败'));
}
resolve(rows);
});
});
const sql = `SELECT * FROM proxies ORDER BY name ASC`;
try {
const db = await getDbInstance();
const rows = await allDb<DbProxyRow>(db, sql);
return rows;
} catch (err: any) {
console.error('Repository: 查询代理列表时出错:', err.message);
throw new Error('获取代理列表失败');
}
};
/**
* 根据 ID 获取单个代理
*/
export const findProxyById = async (id: number): Promise<ProxyData | null> => {
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM proxies WHERE id = ?`, [id], (err, row: ProxyData) => {
if (err) {
console.error(`Repository: 查询代理 ${id} 时出错:`, err.message);
return reject(new Error('获取代理信息失败'));
}
resolve(row || null);
});
});
const sql = `SELECT * FROM proxies WHERE id = ?`;
try {
const db = await getDbInstance();
const row = await getDbRow<DbProxyRow>(db, sql, [id]);
return row || null;
} catch (err: any) {
console.error(`Repository: 查询代理 ${id} 时出错:`, err.message);
throw new Error('获取代理信息失败');
}
};
@@ -106,8 +109,10 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
const fieldsToUpdate: { [key: string]: any } = { ...data };
const params: any[] = [];
// Remove fields that should not be updated directly
delete fieldsToUpdate.id;
delete fieldsToUpdate.created_at;
// updated_at will be set explicitly
fieldsToUpdate.updated_at = Math.floor(Date.now() / 1000);
@@ -115,39 +120,37 @@ export const updateProxy = async (id: number, data: Partial<Omit<ProxyData, 'id'
Object.values(fieldsToUpdate).forEach(value => params.push(value ?? null));
if (!setClauses) {
return false;
console.warn(`[Repository] updateProxy called for ID ${id} with no fields to update.`);
return false; // Nothing to update
}
params.push(id);
params.push(id); // Add the ID for the WHERE clause
return new Promise((resolve, reject) => {
const stmt = db.prepare(`UPDATE proxies SET ${setClauses} WHERE id = ?`);
stmt.run(...params, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 更新代理 ${id} 时出错:`, err.message);
return reject(new Error('更新代理记录失败'));
}
resolve((this as any).changes > 0);
});
});
const sql = `UPDATE proxies 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);
// Handle potential UNIQUE constraint errors if needed
throw new Error('更新代理记录失败');
}
};
/**
* 删除代理
*/
export const deleteProxy = async (id: number): Promise<boolean> => {
return new Promise((resolve, reject) => {
// 注意:connections 表中的 proxy_id 外键设置了 ON DELETE SET NULL
// 所以删除代理时,关联的连接会自动将 proxy_id 设为 NULL。
const stmt = db.prepare(`DELETE FROM proxies WHERE id = ?`);
stmt.run(id, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 删除代理 ${id} 时出错:`, err.message);
return reject(new Error('删除代理记录失败'));
}
resolve((this as any).changes > 0);
});
});
// Note: connections table proxy_id foreign key has ON DELETE SET NULL.
const sql = `DELETE FROM proxies WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 删除代理 ${id} 时出错:`, err.message);
throw new Error('删除代理记录失败');
}
};
@@ -1,4 +1,5 @@
import { getDb } from '../database';
// packages/backend/src/repositories/quick-commands.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
// 定义快捷指令的接口
export interface QuickCommand {
@@ -10,124 +11,118 @@ export interface QuickCommand {
updated_at: number; // Unix 时间戳 (秒)
}
// Define the expected row structure from the database if it matches QuickCommand
type DbQuickCommandRow = QuickCommand;
/**
* 添加一条新的快捷指令
* @param name - 指令名称 (可选)
* @param command - 指令内容
* @returns 返回插入记录的 ID
*/
export const addQuickCommand = (name: string | null, command: string): Promise<number> => {
const db = getDb();
export const addQuickCommand = async (name: string | null, command: string): Promise<number> => {
const sql = `INSERT INTO quick_commands (name, command, created_at, updated_at) VALUES (?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
return new Promise((resolve, reject) => {
db.run(sql, [name, command], function (err) {
if (err) {
console.error('添加快捷指令时出错:', err);
return reject(new Error('无法添加快捷指令'));
}
resolve(this.lastID);
});
});
};
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, command]);
// 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('添加快捷指令时出错:', err.message);
throw new Error('无法添加快捷指令');
}
}; // End of addQuickCommand
/**
* 更新指定的快捷指令
* @param id - 要更新的记录 ID
* @param name - 新的指令名称 (可选)
* @param command - 新的指令内容
* @returns 返回更新的行数 (通常是 1 或 0)
* @returns 返回是否成功更新 (true/false)
*/
export const updateQuickCommand = (id: number, name: string | null, command: string): Promise<number> => {
const db = getDb();
export const updateQuickCommand = async (id: number, name: string | null, command: string): Promise<boolean> => {
const sql = `UPDATE quick_commands SET name = ?, command = ?, updated_at = strftime('%s', 'now') WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [name, command, id], function (err) {
if (err) {
console.error('更新快捷指令时出错:', err);
return reject(new Error('无法更新快捷指令'));
}
resolve(this.changes);
});
});
};
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, command, id]);
return result.changes > 0;
} catch (err: any) {
console.error('更新快捷指令时出错:', err.message);
throw new Error('无法更新快捷指令');
}
}; // End of updateQuickCommand
/**
* 根据 ID 删除指定的快捷指令
* @param id - 要删除的记录 ID
* @returns 返回删除的行数 (通常是 1 或 0)
* @returns 返回是否成功删除 (true/false)
*/
export const deleteQuickCommand = (id: number): Promise<number> => {
const db = getDb();
export const deleteQuickCommand = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM quick_commands WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [id], function (err) {
if (err) {
console.error('删除快捷指令时出错:', err);
return reject(new Error('无法删除快捷指令'));
}
resolve(this.changes);
});
});
};
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error('删除快捷指令时出错:', err.message);
throw new Error('无法删除快捷指令');
}
}; // End of deleteQuickCommand
/**
* 获取所有快捷指令
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回包含所有快捷指令条目的数组
*/
export const getAllQuickCommands = (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommand[]> => {
const db = getDb();
export const getAllQuickCommands = async (sortBy: 'name' | 'usage_count' = 'name'): Promise<QuickCommand[]> => {
let orderByClause = 'ORDER BY name ASC'; // 默认按名称升序
if (sortBy === 'usage_count') {
orderByClause = 'ORDER BY usage_count DESC, name ASC'; // 按使用频率降序,同频率按名称升序
}
// SQLite 中 NULLS LAST/FIRST 的支持可能不一致,这里简单处理 NULL 名称排在前面
const sql = `SELECT id, name, command, usage_count, created_at, updated_at FROM quick_commands ${orderByClause}`;
return new Promise((resolve, reject) => {
db.all(sql, [], (err, rows: QuickCommand[]) => {
if (err) {
console.error('获取快捷指令时出错:', err);
return reject(new Error('无法获取快捷指令'));
}
resolve(rows);
});
});
};
try {
const db = await getDbInstance();
const rows = await allDb<DbQuickCommandRow>(db, sql);
return rows;
} catch (err: any) {
console.error('获取快捷指令时出错:', err.message);
throw new Error('无法获取快捷指令');
}
}; // End of getAllQuickCommands
/**
* 增加指定快捷指令的使用次数
* @param id - 要增加次数的记录 ID
* @returns 返回更新的行数 (通常是 1 或 0)
* @returns 返回是否成功更新 (true/false)
*/
export const incrementUsageCount = (id: number): Promise<number> => {
const db = getDb();
export const incrementUsageCount = async (id: number): Promise<boolean> => {
const sql = `UPDATE quick_commands SET usage_count = usage_count + 1, updated_at = strftime('%s', 'now') WHERE id = ?`;
return new Promise((resolve, reject) => {
db.run(sql, [id], function (err) {
if (err) {
console.error('增加快捷指令使用次数时出错:', err);
return reject(new Error('无法增加快捷指令使用次数'));
}
resolve(this.changes);
});
});
};
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error('增加快捷指令使用次数时出错:', err.message);
throw new Error('无法增加快捷指令使用次数');
}
}; // End of incrementUsageCount
/**
* 根据 ID 查找快捷指令 (用于编辑前获取数据)
* @param id - 要查找的记录 ID
* @returns 返回找到的快捷指令条目,如果未找到则返回 undefined
*/
export const findQuickCommandById = (id: number): Promise<QuickCommand | undefined> => {
const db = getDb();
export const findQuickCommandById = async (id: number): Promise<QuickCommand | undefined> => {
const sql = `SELECT id, name, command, usage_count, created_at, updated_at FROM quick_commands WHERE id = ?`;
return new Promise((resolve, reject) => {
db.get(sql, [id], (err, row: QuickCommand | undefined) => {
if (err) {
console.error('查找快捷指令时出错:', err);
return reject(new Error('无法查找快捷指令'));
}
resolve(row);
});
});
};
try {
const db = await getDbInstance();
const row = await getDbRow<DbQuickCommandRow>(db, sql, [id]);
return row; // Returns undefined if not found
} catch (err: any) {
console.error('查找快捷指令时出错:', err.message);
throw new Error('无法查找快捷指令');
}
}; // End of findQuickCommandById
@@ -1,92 +1,95 @@
import { getDb } from '../database'; // 正确导入 getDb 函数
// packages/backend/src/repositories/settings.repository.ts
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection'; // Import new async helpers
const db = getDb(); // 获取数据库实例
// Remove top-level db instance
// const db = getDb();
export interface Setting {
key: string;
value: string;
}
// Define the expected row structure from the database if different from Setting
// In this case, it seems Setting matches the SELECT columns.
type DbSettingRow = Setting;
export const settingsRepository = {
async getAllSettings(): Promise<Setting[]> {
return new Promise((resolve, reject) => {
db.all('SELECT key, value FROM settings', (err: any, rows: Setting[]) => { // 添加 err 类型
if (err) {
console.error('[Repository] 获取所有设置时出错:', err); // 更新日志为中文
reject(new Error('获取设置失败')); // 更新错误消息为中文
} else {
resolve(rows);
}
});
});
try {
const db = await getDbInstance();
const rows = await allDb<DbSettingRow>(db, 'SELECT key, value FROM settings');
return rows;
} catch (err: any) {
console.error('[Repository] 获取所有设置时出错:', err.message);
throw new Error('获取设置失败');
}
},
async getSetting(key: string): Promise<string | null> {
return new Promise((resolve, reject) => {
console.log(`[Repository] Attempting to get setting with key: ${key}`); // +++ 添加日志 +++
db.get('SELECT value FROM settings WHERE key = ?', [key], (err: any, row: { value: string } | undefined) => { // 添加 err 类型
if (err) {
console.error(`[Repository] 获取设置项 ${key} 时出错:`, err); // 更新日志为中文
reject(new Error(`获取设置项 ${key} 失败`)); // 更新错误消息为中文
} else {
console.log(`[Repository] Found value for key ${key}:`, row ? row.value : null); // +++ 添加日志 +++
resolve(row ? row.value : null);
}
});
});
console.log(`[Repository] Attempting to get setting with key: ${key}`);
try {
const db = await getDbInstance();
// Use the correct type for the expected row structure
const row = await getDbRow<{ value: string }>(db, 'SELECT value FROM settings WHERE key = ?', [key]);
const value = row ? row.value : null;
console.log(`[Repository] Found value for key ${key}:`, value);
return value;
} catch (err: any) {
console.error(`[Repository] 获取设置项 ${key} 时出错:`, err.message);
throw new Error(`获取设置项 ${key} 失败`);
}
},
async setSetting(key: string, value: string): Promise<void> {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000); // 获取当前 Unix 时间戳
const sql = `INSERT INTO settings (key, value, created_at, updated_at)
const now = Math.floor(Date.now() / 1000); // Use seconds
const sql = `INSERT INTO settings (key, value, created_at, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at`;
const params = [key, value, now, now];
const params = [key, value, now, now];
console.log(`[Repository] Attempting to set setting. Key: ${key}, Value: ${value}`); // +++ 添加日志 +++
console.log(`[Repository] Executing SQL: ${sql} with params: ${JSON.stringify(params)}`); // +++ 添加日志 +++
console.log(`[Repository] Attempting to set setting. Key: ${key}, Value: ${value}`);
console.log(`[Repository] Executing SQL: ${sql} with params: ${JSON.stringify(params)}`);
db.run(
sql,
params,
function (this: any, err: any) { // 使用 this 需要 function 声明, 添加 err 类型
if (err) {
console.error(`[Repository] 设置设置项 ${key} 时出错:`, err); // 更新日志为中文
reject(new Error(`设置设置项 ${key} 失败`)); // 更新错误消息为中文
} else {
// this.changes 提供了受影响的行数 (对于 INSERT/UPDATE)
console.log(`[Repository] Successfully set setting for key: ${key}. Rows affected: ${this.changes}`); // +++ 添加日志 +++
resolve();
}
}
);
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, params);
console.log(`[Repository] Successfully set setting for key: ${key}. Rows affected: ${result.changes}`);
} catch (err: any) {
console.error(`[Repository] 设置设置项 ${key} 时出错:`, err.message);
throw new Error(`设置设置项 ${key} 失败`);
}
},
async deleteSetting(key: string): Promise<void> {
return new Promise((resolve, reject) => {
console.log(`[Repository] Attempting to delete setting with key: ${key}`); // +++ 添加日志 +++
db.run('DELETE FROM settings WHERE key = ?', [key], function (this: any, err: any) { // 添加 err 类型
if (err) {
console.error(`[Repository] 删除设置项 ${key} 时出错:`, err); // 更新日志为中文
reject(new Error(`删除设置项 ${key} 失败`)); // 更新错误消息为中文
} else {
console.log(`[Repository] Successfully deleted setting for key: ${key}. Rows affected: ${this.changes}`); // +++ 添加日志 +++
resolve();
}
});
});
async deleteSetting(key: string): Promise<boolean> { // Return boolean indicating success
console.log(`[Repository] Attempting to delete setting with key: ${key}`);
const sql = 'DELETE FROM settings WHERE key = ?';
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [key]);
console.log(`[Repository] Successfully deleted setting for key: ${key}. Rows affected: ${result.changes}`);
return result.changes > 0; // Return true if a row was deleted
} catch (err: any) {
console.error(`[Repository] 删除设置项 ${key} 时出错:`, err.message);
throw new Error(`删除设置项 ${key} 失败`);
}
},
async setMultipleSettings(settings: Record<string, string>): Promise<void> {
console.log('[Repository] setMultipleSettings called with:', JSON.stringify(settings)); // +++ 添加日志 +++
console.log('[Repository] setMultipleSettings called with:', JSON.stringify(settings));
// Use Promise.all with the async setSetting method
// Note: 'this' inside map refers to the settingsRepository object correctly here
const promises = Object.entries(settings).map(([key, value]) =>
this.setSetting(key, value) // this 指向 settingsRepository 对象
this.setSetting(key, value)
);
await Promise.all(promises);
console.log('[Repository] setMultipleSettings finished.'); // +++ 添加日志 +++
try {
await Promise.all(promises);
console.log('[Repository] setMultipleSettings finished successfully.');
} catch (error) {
console.error('[Repository] setMultipleSettings failed:', error);
// Re-throw the error or handle it as needed
throw new Error('批量设置失败');
}
},
};
@@ -1,44 +1,46 @@
import { Database, Statement } from 'sqlite3';
import { getDb } from '../database';
// packages/backend/src/repositories/tag.repository.ts
import { Database, Statement } from 'sqlite3'; // Keep Statement if using prepare directly, otherwise remove
// Import new async helpers and the instance getter
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
const db = getDb();
// Remove top-level db instance
// const db = getDb();
// 定义 Tag 类型 (可以共享到 types 文件)
// Let's assume TagData is the correct interface for a row from the 'tags' table
export interface TagData {
id: number;
name: string;
created_at: number;
updated_at: number; // Assuming tags also have updated_at based on migrations
updated_at: number;
}
/**
* 获取所有标签
*/
export const findAllTags = async (): Promise<TagData[]> => {
return new Promise((resolve, reject) => {
db.all(`SELECT * FROM tags ORDER BY name ASC`, [], (err, rows: TagData[]) => {
if (err) {
console.error('Repository: 查询标签列表时出错:', err.message);
return reject(new Error('获取标签列表失败'));
}
resolve(rows);
});
});
try {
const db = await getDbInstance();
const rows = await allDb<TagData>(db, `SELECT * FROM tags ORDER BY name ASC`);
return rows;
} catch (err: any) {
console.error('Repository: 查询标签列表时出错:', err.message);
throw new Error('获取标签列表失败');
}
};
/**
* 根据 ID 获取单个标签
*/
export const findTagById = async (id: number): Promise<TagData | null> => {
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM tags WHERE id = ?`, [id], (err, row: TagData) => {
if (err) {
console.error(`Repository: 查询标签 ${id} 时出错:`, err.message);
return reject(new Error('获取标签信息失败'));
}
resolve(row || null);
});
});
try {
const db = await getDbInstance();
const row = await getDbRow<TagData>(db, `SELECT * FROM tags WHERE id = ?`, [id]);
return row || null;
} catch (err: any) {
console.error(`Repository: 查询标签 ${id} 时出错:`, err.message);
throw new Error('获取标签信息失败');
}
};
@@ -46,59 +48,59 @@ export const findTagById = async (id: number): Promise<TagData | null> => {
* 创建新标签
*/
export const createTag = async (name: string): Promise<number> => {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(
`INSERT INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)`
);
stmt.run(name, now, now, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
// Handle unique constraint error specifically if needed
console.error('Repository: 创建标签时出错:', err.message);
return reject(new Error(`创建标签失败: ${err.message}`));
}
resolve((this as any).lastID);
});
});
const now = Math.floor(Date.now() / 1000); // Use seconds for consistency? Check table definition
const sql = `INSERT INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, now, now]);
// 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);
// Handle unique constraint error specifically if needed
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`标签名称 "${name}" 已存在。`);
}
throw new Error(`创建标签失败: ${err.message}`);
}
};
/**
* 更新标签名称
*/
export const updateTag = async (id: number, name: string): Promise<boolean> => {
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const stmt = db.prepare(
`UPDATE tags SET name = ?, updated_at = ? WHERE id = ?`
);
stmt.run(name, now, id, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
// Handle unique constraint error specifically if needed
console.error(`Repository: 更新标签 ${id} 时出错:`, err.message);
return reject(new Error(`更新标签失败: ${err.message}`));
}
resolve((this as any).changes > 0);
});
});
const now = Math.floor(Date.now() / 1000);
const sql = `UPDATE tags SET name = ?, updated_at = ? WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, now, id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 更新标签 ${id} 时出错:`, err.message);
// Handle unique constraint error specifically if needed
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`标签名称 "${name}" 已存在。`);
}
throw new Error(`更新标签失败: ${err.message}`);
}
};
/**
* 删除标签
*/
export const deleteTag = async (id: number): Promise<boolean> => {
return new Promise((resolve, reject) => {
// Note: connection_tags junction table has ON DELETE CASCADE for tag_id,
// so related entries there will be deleted automatically.
const stmt = db.prepare(`DELETE FROM tags WHERE id = ?`);
stmt.run(id, function (this: Statement, err: Error | null) {
stmt.finalize();
if (err) {
console.error(`Repository: 删除标签 ${id} 时出错:`, err.message);
return reject(new Error('删除标签失败'));
}
resolve((this as any).changes > 0);
});
});
// Note: connection_tags junction table has ON DELETE CASCADE for tag_id,
// so related entries there will be deleted automatically.
const sql = `DELETE FROM tags WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error(`Repository: 删除标签 ${id} 时出错:`, err.message);
throw new Error('删除标签失败');
}
};
@@ -1,50 +1,94 @@
import { getDb } from '../database';
// packages/backend/src/repositories/terminal-theme.repository.ts
import { Database } from 'sqlite3'; // Import Database type if needed for type hints
import { getDbInstance, runDb, getDb, allDb } from '../database/connection'; // Import new async helpers, including getDb
// Remove the incorrect import of DbTerminalThemeRow
import { TerminalTheme, CreateTerminalThemeDto, UpdateTerminalThemeDto } from '../types/terminal-theme.types';
import { defaultXtermTheme } from '../config/default-themes'; // 假设默认主题配置在此
import { defaultXtermTheme } from '../config/default-themes';
// const db = getDb(); // Removed top-level call to avoid circular dependency issues
// Define the interface for the raw database row structure
// Interface matching the schema in schema.ts
interface DbTerminalThemeRow {
id: number;
name: string;
theme_type: 'preset' | 'user';
foreground?: string | null;
background?: string | null;
cursor?: string | null;
cursor_accent?: string | null;
selection_background?: string | null;
black?: string | null;
red?: string | null;
green?: string | null;
yellow?: string | null;
blue?: string | null;
magenta?: string | null;
cyan?: string | null;
white?: string | null;
bright_black?: string | null;
bright_red?: string | null;
bright_green?: string | null;
bright_yellow?: string | null;
bright_blue?: string | null;
bright_magenta?: string | null;
bright_cyan?: string | null;
bright_white?: string | null;
created_at: number;
updated_at: number;
}
/**
* SQL语句:创建 terminal_themes 表
*/
export const SQL_CREATE_TABLE = `
CREATE TABLE IF NOT EXISTS terminal_themes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
theme_data TEXT NOT NULL, -- Store ITheme as JSON string
is_preset BOOLEAN NOT NULL DEFAULT 0,
preset_key TEXT NULL UNIQUE, -- 可选,用于识别预设主题
is_system_default BOOLEAN NOT NULL DEFAULT 0, -- 新增:标记是否为系统默认主题
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`;
/**
* 创建 terminal_themes 表 (如果不存在) - 不再自动调用
*/
const createTableIfNotExists = () => {
// This function is no longer called automatically, initialization is handled in database.ts
getDb().run(SQL_CREATE_TABLE, (err) => {
if (err) {
console.error('创建 terminal_themes 表失败:', err.message);
} else {
console.log('terminal_themes 表已存在或已创建。');
}
});
};
// SQL_CREATE_TABLE and createTableIfNotExists removed as initialization is handled in database/connection.ts
// 辅助函数:将数据库行转换为 TerminalTheme 对象
const mapRowToTerminalTheme = (row: any): TerminalTheme => {
return {
_id: row.id.toString(), // SQLite ID 是数字,转换为字符串以匹配 NeDB 风格
name: row.name,
themeData: JSON.parse(row.theme_data), // 解析 JSON 字符串
isPreset: !!row.is_preset, // 转换为布尔值
isSystemDefault: !!row.is_system_default, // 映射新增的列
createdAt: row.created_at,
updatedAt: row.updated_at,
};
// Add type annotation for the input row
const mapRowToTerminalTheme = (row: DbTerminalThemeRow): TerminalTheme => {
// Basic check if row exists and has id property
if (!row || typeof row.id === 'undefined') {
console.error("mapRowToTerminalTheme received invalid row:", row);
// Return a default or throw an error, depending on desired behavior
// For now, let's throw an error to make the issue visible
throw new Error("Invalid database row provided to mapRowToTerminalTheme");
}
try {
return {
_id: row.id.toString(),
name: row.name,
// Reconstruct themeData from individual columns
themeData: {
foreground: row.foreground ?? undefined,
background: row.background ?? undefined,
cursor: row.cursor ?? undefined,
cursorAccent: row.cursor_accent ?? undefined,
selectionBackground: row.selection_background ?? undefined,
black: row.black ?? undefined,
red: row.red ?? undefined,
green: row.green ?? undefined,
yellow: row.yellow ?? undefined,
blue: row.blue ?? undefined,
magenta: row.magenta ?? undefined,
cyan: row.cyan ?? undefined,
white: row.white ?? undefined,
brightBlack: row.bright_black ?? undefined,
brightRed: row.bright_red ?? undefined,
brightGreen: row.bright_green ?? undefined,
brightYellow: row.bright_yellow ?? undefined,
brightBlue: row.bright_blue ?? undefined,
brightMagenta: row.bright_magenta ?? undefined,
brightCyan: row.bright_cyan ?? undefined,
brightWhite: row.bright_white ?? undefined,
},
isPreset: row.theme_type === 'preset',
// isSystemDefault needs to be handled differently, maybe based on name 'default'?
// For now, let's assume it's not directly mapped or needed here.
isSystemDefault: row.name === 'default', // Tentative mapping based on name
createdAt: row.created_at,
updatedAt: row.updated_at,
};
} catch (e: any) {
// Log the entire row for debugging instead of the non-existent theme_data
console.error(`Error mapping theme data for theme ID ${row.id}:`, e.message, "Raw row:", row);
// Return a partially mapped object or throw error
throw new Error(`Failed to map theme data for theme ID ${row.id}`);
}
};
/**
@@ -52,34 +96,45 @@ const mapRowToTerminalTheme = (row: any): TerminalTheme => {
* @returns Promise<TerminalTheme[]>
*/
export const findAllThemes = async (): Promise<TerminalTheme[]> => {
return new Promise((resolve, reject) => {
getDb().all('SELECT * FROM terminal_themes ORDER BY is_preset DESC, name ASC', [], (err, rows) => {
if (err) {
console.error('查询所有终端主题失败:', err.message);
reject(new Error('查询终端主题失败'));
} else {
resolve(rows.map(mapRowToTerminalTheme));
}
});
});
try {
const db = await getDbInstance();
// Specify the expected row type for allDb
const rows = await allDb<DbTerminalThemeRow>(db, 'SELECT * FROM terminal_themes ORDER BY is_preset DESC, name ASC');
// Filter out potential errors during mapping
return rows.map(row => {
try {
return mapRowToTerminalTheme(row);
} catch (mapError: any) {
console.error(`Error mapping row ID ${row?.id}:`, mapError.message);
return null; // Or handle differently
}
}).filter((theme): theme is TerminalTheme => theme !== null);
} catch (err: any) { // Add type annotation for err
console.error('查询所有终端主题失败:', err.message);
throw new Error('查询终端主题失败'); // Re-throw or handle error appropriately
}
};
/**
* 根据 ID 查找终端主题
* @param id 主题 ID (注意:这里是 SQLite 数字 ID)
* @param id 主题 ID (SQLite 数字 ID)
* @returns Promise<TerminalTheme | null>
*/
export const findThemeById = async (id: number): Promise<TerminalTheme | null> => {
return new Promise((resolve, reject) => {
getDb().get('SELECT * FROM terminal_themes WHERE id = ?', [id], (err, row) => {
if (err) {
console.error(`查询 ID 为 ${id} 的终端主题失败:`, err.message);
reject(new Error('查询终端主题失败'));
} else {
resolve(row ? mapRowToTerminalTheme(row) : null);
}
});
});
if (isNaN(id) || id <= 0) {
console.error("findThemeById called with invalid ID:", id);
return null; // Return null for invalid IDs
}
try {
const db = await getDbInstance();
// Specify the expected row type for getDbRow
// Use getDb instead of the non-existent getDbRow
const row = await getDb<DbTerminalThemeRow>(db, 'SELECT * FROM terminal_themes WHERE id = ?', [id]);
return row ? mapRowToTerminalTheme(row) : null;
} catch (err: any) { // Add type annotation for err
console.error(`查询 ID 为 ${id} 的终端主题失败:`, err.message);
throw new Error('查询终端主题失败');
}
};
/**
@@ -89,36 +144,33 @@ export const findThemeById = async (id: number): Promise<TerminalTheme | null> =
*/
export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<TerminalTheme> => {
const now = Date.now();
const themeDataJson = JSON.stringify(themeDto.themeData); // 将 ITheme 转换为 JSON 字符串
const themeDataJson = JSON.stringify(themeDto.themeData);
const sql = `
INSERT INTO terminal_themes (name, theme_data, is_preset, created_at, updated_at)
VALUES (?, ?, 0, ?, ?)
`;
return new Promise((resolve, reject) => {
getDb().run(sql, [themeDto.name, themeDataJson, now, now], function (err) {
if (err) {
console.error('创建新终端主题失败:', err.message);
// 特别处理唯一约束错误
if (err.message.includes('UNIQUE constraint failed')) {
reject(new Error(`主题名称 "${themeDto.name}" 已存在。`));
} else {
reject(new Error('创建终端主题失败'));
}
} else {
// 获取新插入行的 ID 并查询返回完整对象
findThemeById(this.lastID)
.then(newTheme => {
if (newTheme) {
resolve(newTheme);
} else {
// 理论上不应该发生,但作为回退
reject(new Error('创建主题后未能检索到该主题'));
}
})
.catch(reject);
}
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [themeDto.name, themeDataJson, now, now]);
// Ensure lastID is valid before trying to find the theme
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('创建主题后未能获取有效的 lastID');
}
const newTheme = await findThemeById(result.lastID);
if (newTheme) {
return newTheme;
} else {
// This case might happen if findThemeById fails for some reason
throw new Error(`创建主题后未能检索到 ID 为 ${result.lastID} 的主题`);
}
} catch (err: any) { // Add type annotation for err
console.error('创建新终端主题失败:', err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`主题名称 "${themeDto.name}" 已存在。`);
} else {
throw new Error('创建终端主题失败');
}
}
};
/**
@@ -130,26 +182,23 @@ export const createTheme = async (themeDto: CreateTerminalThemeDto): Promise<Ter
export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto): Promise<boolean> => {
const now = Date.now();
const themeDataJson = JSON.stringify(themeDto.themeData);
// 只允许更新非预设主题的 name 和 theme_data
const sql = `
UPDATE terminal_themes
SET name = ?, theme_data = ?, updated_at = ?
WHERE id = ? AND is_preset = 0
`;
return new Promise((resolve, reject) => {
getDb().run(sql, [themeDto.name, themeDataJson, now, id], function (err) {
if (err) {
console.error(`更新 ID 为 ${id} 的终端主题失败:`, err.message);
if (err.message.includes('UNIQUE constraint failed')) {
reject(new Error(`主题名称 "${themeDto.name}" 已存在。`));
} else {
reject(new Error('更新终端主题失败'));
}
} else {
resolve(this.changes > 0); // 如果有行被改变,则更新成功
}
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [themeDto.name, themeDataJson, now, id]);
return result.changes > 0;
} catch (err: any) { // Add type annotation for err
console.error(`更新 ID 为 ${id} 的终端主题失败:`, err.message);
if (err.message.includes('UNIQUE constraint failed')) {
throw new Error(`主题名称 "${themeDto.name}" 已存在。`);
} else {
throw new Error('更新终端主题失败');
}
}
};
/**
@@ -158,77 +207,66 @@ export const updateTheme = async (id: number, themeDto: UpdateTerminalThemeDto):
* @returns Promise<boolean> 是否成功删除
*/
export const deleteTheme = async (id: number): Promise<boolean> => {
// 只允许删除非预设主题
const sql = 'DELETE FROM terminal_themes WHERE id = ? AND is_preset = 0';
return new Promise((resolve, reject) => {
getDb().run(sql, [id], function (err) {
if (err) {
console.error(`删除 ID 为 ${id} 的终端主题失败:`, err.message);
reject(new Error('删除终端主题失败'));
} else {
resolve(this.changes > 0); // 如果有行被改变,则删除成功
}
});
});
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) { // Add type annotation for err
console.error(`删除 ID 为 ${id} 的终端主题失败:`, err.message);
throw new Error('删除终端主题失败');
}
};
/**
* 初始化预设主题到数据库 (如果不存在)
* 这个函数应该在数据库连接成功后,由应用初始化逻辑调用。
* @param presets 预设主题定义数组 (包含 name, themeData, isPreset=true, 可选 preset_key)
* @param presets 预设主题定义数组
*/
export const initializePresetThemes = async (presets: Array<Omit<TerminalTheme, '_id' | 'createdAt' | 'updatedAt'> & { preset_key?: string }>) => {
export const initializePresetThemes = async (db: Database, presets: Array<Omit<TerminalTheme, '_id' | 'createdAt' | 'updatedAt' | 'isSystemDefault'> & { name: string }>) => {
console.log('[DB Init] 开始检查并初始化预设主题...');
const now = Date.now();
const nowSeconds = Math.floor(Date.now() / 1000); // Use seconds for DB consistency
// const db = await getDbInstance(); // Use the passed db instance
// 使用 for...of 循环确保顺序执行检查和插入(避免并发 UNIQUE 约束问题)
for (const preset of presets) {
await new Promise<void>((resolve, reject) => {
// 优先使用 preset_key 检查,如果提供了的话
const checkColumn = preset.preset_key ? 'preset_key' : 'name';
const checkValue = preset.preset_key ?? preset.name;
try {
// Check using name and theme_type
const existing = await getDb<{ id: number }>(db, `SELECT id FROM terminal_themes WHERE name = ? AND theme_type = 'preset'`, [preset.name]);
getDb().get(`SELECT id FROM terminal_themes WHERE ${checkColumn} = ? AND is_preset = 1`, [checkValue], (err, row) => {
if (err) {
console.error(`[DB Init] 检查预设主题 "${preset.name}" (Key: ${checkValue}) 时出错:`, err.message);
return reject(err);
}
if (!row) {
const themeDataJson = JSON.stringify(preset.themeData);
const isDefault = preset.preset_key === 'default' ? 1 : 0;
// 始终包含 preset_key 列,如果不存在则插入 NULL
const columns = ['name', 'theme_data', 'is_preset', 'is_system_default', 'preset_key', 'created_at', 'updated_at']; // 7 columns
const values = [preset.name, themeDataJson, 1, isDefault, preset.preset_key ?? null, now, now]; // 7 values
const placeholders = ['?', '?', '?', '?', '?', '?', '?']; // 7 placeholders
if (!existing) {
// Map preset.themeData to individual columns
const theme = preset.themeData;
const columns = [
'name', 'theme_type', 'foreground', 'background', 'cursor', 'cursor_accent',
'selection_background', 'black', 'red', 'green', 'yellow', 'blue',
'magenta', 'cyan', 'white', 'bright_black', 'bright_red', 'bright_green',
'bright_yellow', 'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white',
'created_at', 'updated_at'
];
const values = [
preset.name, 'preset', theme?.foreground, theme?.background, theme?.cursor, theme?.cursorAccent,
theme?.selectionBackground, theme?.black, theme?.red, theme?.green, theme?.yellow, theme?.blue,
theme?.magenta, theme?.cyan, theme?.white, theme?.brightBlack, theme?.brightRed, theme?.brightGreen,
theme?.brightYellow, theme?.brightBlue, theme?.brightMagenta, theme?.brightCyan, theme?.brightWhite,
nowSeconds, nowSeconds
];
const placeholders = columns.map(() => '?').join(', ');
// 移除动态添加 preset_key 的逻辑
// if (preset.preset_key) {
// values.push(preset.preset_key);
// placeholders.push('?');
// }
const insertSql = `
INSERT INTO terminal_themes (${columns.join(', ')})
VALUES (${placeholders.join(', ')})
`;
getDb().run(insertSql, values, (insertErr) => {
if (insertErr) {
console.error(`[DB Init] 初始化预设主题 "${preset.name}" (Key: ${preset.preset_key ?? 'N/A'}) 失败:`, insertErr.message); // 调整日志输出
return reject(insertErr);
} else {
console.log(`[DB Init] 预设主题 "${preset.name}" (Key: ${checkValue}) 已初始化到数据库。`);
resolve();
}
});
} else {
// console.log(`[DB Init] 预设主题 "${preset.name}" (Key: ${checkValue}) 已存在,跳过初始化。`);
resolve();
}
});
});
const insertSql = `
INSERT INTO terminal_themes (${columns.join(', ')})
VALUES (${placeholders})
`;
await runDb(db, insertSql, values);
console.log(`[DB Init] 预设主题 "${preset.name}" 已初始化到数据库。`);
} else {
// console.log(`[DB Init] 预设主题 "${preset.name}" 已存在,跳过初始化。`);
}
} catch (err: any) { // Add type annotation for err
// Remove reference to non-existent preset_key
console.error(`[DB Init] 处理预设主题 "${preset.name}" 时出错:`, err.message);
// Decide if one error should stop the whole process or just log and continue
// throw err; // Uncomment to stop on first error
}
}
console.log('[DB Init] 预设主题检查和初始化完成。');
};
// 移除所有在此文件中的初始化调用和相关导入,它们应该在 database.ts 或 app.ts 中进行