mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-03 11:18:38 +00:00
- Implemented githubTokensDb for managing GitHub tokens with CRUD operations. - Created otificationPreferencesDb to handle user notification preferences. - Added projectsDb for project path management and related operations. - Introduced pushSubscriptionsDb for managing browser push subscriptions. - Developed scanStateDb to track the last scanned timestamp. - Established sessionsDb for session management with CRUD functionalities. - Created userDb for user management, including authentication and onboarding. - Implemented apidKeysDb for storing and managing VAPID keys. feat(database): define schema for new database tables - Added SQL schema definitions for users, API keys, user credentials, notification preferences, VAPID keys, push subscriptions, projects, sessions, scan state, and app configuration. - Included necessary indexes for performance optimization. refactor(shared): enhance type definitions and utility functions - Updated shared types and interfaces for improved clarity and consistency. - Added new types for credential management and provider-specific operations. - Refined utility functions for better error handling and message normalization.
293 lines
9.6 KiB
TypeScript
293 lines
9.6 KiB
TypeScript
import { Database } from 'better-sqlite3';
|
|
|
|
import {
|
|
APP_CONFIG_TABLE_SCHEMA_SQL,
|
|
LAST_SCANNED_AT_SQL,
|
|
PROJECTS_TABLE_SCHEMA_SQL,
|
|
PUSH_SUBSCRIPTIONS_TABLE_SCHEMA_SQL,
|
|
SESSIONS_TABLE_SCHEMA_SQL,
|
|
USER_NOTIFICATION_PREFERENCES_TABLE_SCHEMA_SQL,
|
|
VAPID_KEYS_TABLE_SCHEMA_SQL,
|
|
} from '@/modules/database/schema.js';
|
|
|
|
const SQLITE_UUID_SQL = `
|
|
lower(hex(randomblob(4))) || '-' ||
|
|
lower(hex(randomblob(2))) || '-' ||
|
|
lower(hex(randomblob(2))) || '-' ||
|
|
lower(hex(randomblob(2))) || '-' ||
|
|
lower(hex(randomblob(6)))
|
|
`;
|
|
|
|
type TableInfoRow = {
|
|
name: string;
|
|
pk: number;
|
|
};
|
|
|
|
const addColumnToTableIfNotExists = (
|
|
db: Database,
|
|
tableName: string,
|
|
columnNames: string[],
|
|
columnName: string,
|
|
columnType: string
|
|
) => {
|
|
if (!columnNames.includes(columnName)) {
|
|
console.log(`Running migration: Adding ${columnName} column to ${tableName} table`);
|
|
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`);
|
|
}
|
|
};
|
|
|
|
const tableExists = (db: Database, tableName: string): boolean =>
|
|
Boolean(
|
|
db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
.get(tableName)
|
|
);
|
|
|
|
const getTableInfo = (db: Database, tableName: string): TableInfoRow[] =>
|
|
db.prepare(`PRAGMA table_info(${tableName})`).all() as TableInfoRow[];
|
|
|
|
const migrateLegacySessionNames = (db: Database): void => {
|
|
const hasLegacySessionNamesTable = tableExists(db, 'session_names');
|
|
const hasSessionsTable = tableExists(db, 'sessions');
|
|
|
|
if (!hasLegacySessionNamesTable) {
|
|
return;
|
|
}
|
|
|
|
if (hasSessionsTable) {
|
|
console.log('Running migration: Merging session_names into sessions');
|
|
db.exec(`
|
|
INSERT OR REPLACE INTO sessions (session_id, provider, custom_name, created_at, updated_at)
|
|
SELECT
|
|
session_id,
|
|
COALESCE(provider, 'claude'),
|
|
custom_name,
|
|
COALESCE(created_at, CURRENT_TIMESTAMP),
|
|
COALESCE(updated_at, CURRENT_TIMESTAMP)
|
|
FROM session_names
|
|
`);
|
|
db.exec('DROP TABLE session_names');
|
|
return;
|
|
}
|
|
|
|
console.log('Running migration: Renaming session_names table to sessions');
|
|
db.exec('ALTER TABLE session_names RENAME TO sessions');
|
|
};
|
|
|
|
const migrateLegacyWorkspaceTableIntoProjects = (db: Database): void => {
|
|
db.exec(PROJECTS_TABLE_SCHEMA_SQL);
|
|
|
|
if (!tableExists(db, 'workspace_original_paths')) {
|
|
return;
|
|
}
|
|
|
|
console.log('Running migration: Migrating workspace_original_paths data into projects');
|
|
db.exec(`
|
|
INSERT INTO projects (project_id, project_path, custom_project_name, isStarred, isArchived)
|
|
SELECT
|
|
CASE
|
|
WHEN workspace_id IS NULL OR trim(workspace_id) = ''
|
|
THEN ${SQLITE_UUID_SQL}
|
|
ELSE workspace_id
|
|
END,
|
|
workspace_path,
|
|
custom_workspace_name,
|
|
COALESCE(isStarred, 0),
|
|
0
|
|
FROM workspace_original_paths
|
|
WHERE workspace_path IS NOT NULL AND trim(workspace_path) <> ''
|
|
ON CONFLICT(project_path) DO UPDATE SET
|
|
custom_project_name = COALESCE(projects.custom_project_name, excluded.custom_project_name),
|
|
isStarred = COALESCE(projects.isStarred, excluded.isStarred)
|
|
`);
|
|
};
|
|
|
|
const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
|
|
const hasSessions = tableExists(db, 'sessions');
|
|
if (!hasSessions) {
|
|
db.exec(SESSIONS_TABLE_SCHEMA_SQL);
|
|
return;
|
|
}
|
|
|
|
const sessionsTableInfo = getTableInfo(db, 'sessions');
|
|
const columnNames = sessionsTableInfo.map((column) => column.name);
|
|
const primaryKeyColumns = sessionsTableInfo
|
|
.filter((column) => column.pk > 0)
|
|
.sort((a, b) => a.pk - b.pk)
|
|
.map((column) => column.name);
|
|
|
|
const shouldRebuild =
|
|
!columnNames.includes('project_path') ||
|
|
primaryKeyColumns.length !== 2 ||
|
|
primaryKeyColumns[0] !== 'session_id' ||
|
|
primaryKeyColumns[1] !== 'provider';
|
|
|
|
if (!shouldRebuild) {
|
|
addColumnToTableIfNotExists(db, 'sessions', columnNames, 'jsonl_path', 'TEXT');
|
|
addColumnToTableIfNotExists(db, 'sessions', columnNames, 'created_at', 'DATETIME');
|
|
addColumnToTableIfNotExists(db, 'sessions', columnNames, 'updated_at', 'DATETIME');
|
|
db.exec('UPDATE sessions SET created_at = COALESCE(created_at, CURRENT_TIMESTAMP)');
|
|
db.exec('UPDATE sessions SET updated_at = COALESCE(updated_at, CURRENT_TIMESTAMP)');
|
|
return;
|
|
}
|
|
|
|
console.log('Running migration: Rebuilding sessions table to project-based schema');
|
|
|
|
const projectPathExpression = columnNames.includes('project_path')
|
|
? 'project_path'
|
|
: columnNames.includes('workspace_path')
|
|
? 'workspace_path'
|
|
: 'NULL';
|
|
|
|
const providerExpression = columnNames.includes('provider')
|
|
? "COALESCE(provider, 'claude')"
|
|
: "'claude'";
|
|
|
|
const customNameExpression = columnNames.includes('custom_name')
|
|
? 'custom_name'
|
|
: 'NULL';
|
|
|
|
const jsonlPathExpression = columnNames.includes('jsonl_path')
|
|
? 'jsonl_path'
|
|
: 'NULL';
|
|
|
|
const createdAtExpression = columnNames.includes('created_at')
|
|
? 'COALESCE(created_at, CURRENT_TIMESTAMP)'
|
|
: 'CURRENT_TIMESTAMP';
|
|
|
|
const updatedAtExpression = columnNames.includes('updated_at')
|
|
? 'COALESCE(updated_at, CURRENT_TIMESTAMP)'
|
|
: 'CURRENT_TIMESTAMP';
|
|
|
|
db.exec('PRAGMA foreign_keys = OFF');
|
|
try {
|
|
db.exec('BEGIN TRANSACTION');
|
|
db.exec('DROP TABLE IF EXISTS sessions__new');
|
|
db.exec(`
|
|
CREATE TABLE sessions__new (
|
|
session_id TEXT NOT NULL,
|
|
provider TEXT NOT NULL DEFAULT 'claude',
|
|
custom_name TEXT,
|
|
project_path TEXT,
|
|
jsonl_path TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (session_id, provider),
|
|
FOREIGN KEY (project_path) REFERENCES projects(project_path)
|
|
ON DELETE SET NULL
|
|
ON UPDATE CASCADE
|
|
)
|
|
`);
|
|
db.exec(`
|
|
INSERT OR REPLACE INTO sessions__new (
|
|
session_id,
|
|
provider,
|
|
custom_name,
|
|
project_path,
|
|
jsonl_path,
|
|
created_at,
|
|
updated_at
|
|
)
|
|
SELECT
|
|
session_id,
|
|
${providerExpression},
|
|
${customNameExpression},
|
|
${projectPathExpression},
|
|
${jsonlPathExpression},
|
|
${createdAtExpression},
|
|
${updatedAtExpression}
|
|
FROM sessions
|
|
WHERE session_id IS NOT NULL AND trim(session_id) <> ''
|
|
`);
|
|
db.exec('DROP TABLE sessions');
|
|
db.exec('ALTER TABLE sessions__new RENAME TO sessions');
|
|
db.exec('COMMIT');
|
|
} catch (migrationError) {
|
|
db.exec('ROLLBACK');
|
|
throw migrationError;
|
|
} finally {
|
|
db.exec('PRAGMA foreign_keys = ON');
|
|
}
|
|
};
|
|
|
|
const ensureProjectsForSessionPaths = (db: Database): void => {
|
|
if (!tableExists(db, 'sessions')) {
|
|
return;
|
|
}
|
|
|
|
db.exec(`
|
|
INSERT INTO projects (project_id, project_path, custom_project_name, isStarred, isArchived)
|
|
SELECT
|
|
${SQLITE_UUID_SQL},
|
|
project_path,
|
|
NULL,
|
|
0,
|
|
0
|
|
FROM sessions
|
|
WHERE project_path IS NOT NULL AND trim(project_path) <> ''
|
|
ON CONFLICT(project_path) DO NOTHING
|
|
`);
|
|
};
|
|
|
|
export const runMigrations = (db: Database) => {
|
|
try {
|
|
const usersTableInfo = db.prepare('PRAGMA table_info(users)').all() as { name: string }[];
|
|
const userColumnNames = usersTableInfo.map((column) => column.name);
|
|
|
|
addColumnToTableIfNotExists(db, 'users', userColumnNames, 'git_name', 'TEXT');
|
|
addColumnToTableIfNotExists(db, 'users', userColumnNames, 'git_email', 'TEXT');
|
|
addColumnToTableIfNotExists(
|
|
db,
|
|
'users',
|
|
userColumnNames,
|
|
'has_completed_onboarding',
|
|
'BOOLEAN DEFAULT 0'
|
|
);
|
|
|
|
db.exec(APP_CONFIG_TABLE_SCHEMA_SQL);
|
|
db.exec(USER_NOTIFICATION_PREFERENCES_TABLE_SCHEMA_SQL);
|
|
db.exec(VAPID_KEYS_TABLE_SCHEMA_SQL);
|
|
db.exec(PUSH_SUBSCRIPTIONS_TABLE_SCHEMA_SQL);
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(user_id)');
|
|
|
|
db.exec(PROJECTS_TABLE_SCHEMA_SQL);
|
|
const projectsTableInfo = getTableInfo(db, 'projects');
|
|
const projectColumnNames = projectsTableInfo.map((column) => column.name);
|
|
addColumnToTableIfNotExists(db, 'projects', projectColumnNames, 'custom_project_name', 'TEXT DEFAULT NULL');
|
|
addColumnToTableIfNotExists(db, 'projects', projectColumnNames, 'project_id', 'TEXT');
|
|
addColumnToTableIfNotExists(db, 'projects', projectColumnNames, 'isStarred', 'BOOLEAN DEFAULT 0');
|
|
addColumnToTableIfNotExists(db, 'projects', projectColumnNames, 'isArchived', 'BOOLEAN DEFAULT 0');
|
|
db.exec(`
|
|
UPDATE projects
|
|
SET project_id = ${SQLITE_UUID_SQL}
|
|
WHERE project_id IS NULL OR trim(project_id) = ''
|
|
`);
|
|
|
|
migrateLegacyWorkspaceTableIntoProjects(db);
|
|
migrateLegacySessionNames(db);
|
|
rebuildSessionsTableWithProjectSchema(db);
|
|
ensureProjectsForSessionPaths(db);
|
|
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id)');
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path)');
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_starred ON projects(isStarred)');
|
|
db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_archived ON projects(isArchived)');
|
|
|
|
db.exec('DROP INDEX IF EXISTS idx_session_names_lookup');
|
|
db.exec('DROP INDEX IF EXISTS idx_sessions_workspace_path');
|
|
db.exec('DROP INDEX IF EXISTS idx_workspace_original_paths_is_starred');
|
|
db.exec('DROP INDEX IF EXISTS idx_workspace_original_paths_workspace_id');
|
|
|
|
if (tableExists(db, 'workspace_original_paths')) {
|
|
console.log('Running migration: Dropping legacy workspace_original_paths table');
|
|
db.exec('DROP TABLE workspace_original_paths');
|
|
}
|
|
|
|
db.exec(LAST_SCANNED_AT_SQL);
|
|
console.log('Database migrations completed successfully');
|
|
} catch (error: any) {
|
|
console.error('Error running migrations:', error.message);
|
|
throw error;
|
|
}
|
|
};
|