diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index d1f10090..e9a0474e 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -1,5 +1,5 @@ import migrationService from "../services/migrationService"; -import type { QueryExecResult } from "../services/migrationService"; +import type { QueryExecResult, SqlValue } from "../interfaces/database"; // Each migration can include multiple SQL statements (with semicolons) const MIGRATIONS = [ @@ -96,7 +96,10 @@ export async function registerMigrations(): Promise { } export async function runMigrations( - sqlExec: (sql: string, params?: any[]) => Promise>, + sqlExec: ( + sql: string, + params?: SqlValue[], + ) => Promise>, ): Promise { await registerMigrations(); await migrationService.runMigrations(sqlExec); diff --git a/src/db/index.ts b/src/db/index.ts index d25d0ed9..9a73e860 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -90,21 +90,18 @@ db.on("populate", async () => { try { await db.settings.add(DEFAULT_SETTINGS); } catch (error) { - console.error( - "Error populating the database with default settings:", - error, - ); + logger.error("Error populating the database with default settings:", error); } }); // Helper function to safely open the database with retries async function safeOpenDatabase(retries = 1, delay = 500): Promise { - // console.log("Starting safeOpenDatabase with retries:", retries); + // logger.log("Starting safeOpenDatabase with retries:", retries); for (let i = 0; i < retries; i++) { try { - // console.log(`Attempt ${i + 1}: Checking if database is open...`); + // logger.log(`Attempt ${i + 1}: Checking if database is open...`); if (!db.isOpen()) { - // console.log(`Attempt ${i + 1}: Database is closed, attempting to open...`); + // logger.log(`Attempt ${i + 1}: Database is closed, attempting to open...`); // Create a promise that rejects after 5 seconds const timeoutPromise = new Promise((_, reject) => { @@ -113,19 +110,19 @@ async function safeOpenDatabase(retries = 1, delay = 500): Promise { // Race between the open operation and the timeout const openPromise = db.open(); - // console.log(`Attempt ${i + 1}: Waiting for db.open() promise...`); + // logger.log(`Attempt ${i + 1}: Waiting for db.open() promise...`); await Promise.race([openPromise, timeoutPromise]); // If we get here, the open succeeded - // console.log(`Attempt ${i + 1}: Database opened successfully`); + // logger.log(`Attempt ${i + 1}: Database opened successfully`); return; } - // console.log(`Attempt ${i + 1}: Database was already open`); + // logger.log(`Attempt ${i + 1}: Database was already open`); return; } catch (error) { - console.error(`Attempt ${i + 1}: Database open failed:`, error); + logger.error(`Attempt ${i + 1}: Database open failed:`, error); if (i < retries - 1) { - console.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`); + logger.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`); await new Promise((resolve) => setTimeout(resolve, delay)); } else { throw error; @@ -142,12 +139,12 @@ export async function updateDefaultSettings( delete settingsChanges.id; try { try { - // console.log("Database state before open:", db.isOpen() ? "open" : "closed"); - // console.log("Database name:", db.name); - // console.log("Database version:", db.verno); + // logger.log("Database state before open:", db.isOpen() ? "open" : "closed"); + // logger.log("Database name:", db.name); + // logger.log("Database version:", db.verno); await safeOpenDatabase(); } catch (openError: unknown) { - console.error("Failed to open database:", openError, String(openError)); + logger.error("Failed to open database:", openError, String(openError)); throw new Error( `The database connection failed. We recommend you try again or restart the app.`, ); @@ -158,7 +155,7 @@ export async function updateDefaultSettings( ); return result; } catch (error) { - console.error("Error updating default settings:", error); + logger.error("Error updating default settings:", error); if (error instanceof Error) { throw error; // Re-throw if it's already an Error with a message } else { diff --git a/src/db/sqlite/init.ts b/src/db/sqlite/init.ts deleted file mode 100644 index 856971d9..00000000 --- a/src/db/sqlite/init.ts +++ /dev/null @@ -1,293 +0,0 @@ -/** - * SQLite Database Initialization - * - * This module handles database initialization, including: - * - Database connection management - * - Schema creation and migration - * - Connection pooling and lifecycle - * - Error handling and recovery - */ - -import { Database, SQLite3 } from '@wa-sqlite/sql.js'; -import { DATABASE_SCHEMA, SQLiteTable } from './types'; -import { logger } from '../../utils/logger'; - -// ============================================================================ -// Database Connection Management -// ============================================================================ - -export interface DatabaseConnection { - db: Database; - sqlite3: SQLite3; - isOpen: boolean; - lastUsed: number; -} - -let connection: DatabaseConnection | null = null; -const CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes - -/** - * Initialize the SQLite database connection - */ -export async function initDatabase(): Promise { - if (connection?.isOpen) { - connection.lastUsed = Date.now(); - return connection; - } - - try { - const sqlite3 = await import('@wa-sqlite/sql.js'); - const db = await sqlite3.open(':memory:'); // TODO: Configure storage location - - // Enable foreign keys - await db.exec('PRAGMA foreign_keys = ON;'); - - // Configure for better performance - await db.exec(` - PRAGMA journal_mode = WAL; - PRAGMA synchronous = NORMAL; - PRAGMA cache_size = -2000; -- Use 2MB of cache - `); - - connection = { - db, - sqlite3, - isOpen: true, - lastUsed: Date.now() - }; - - // Start connection cleanup interval - startConnectionCleanup(); - - return connection; - } catch (error) { - logger.error('[SQLite] Database initialization failed:', error); - throw new Error('Failed to initialize database'); - } -} - -/** - * Close the database connection - */ -export async function closeDatabase(): Promise { - if (!connection?.isOpen) return; - - try { - await connection.db.close(); - connection.isOpen = false; - connection = null; - } catch (error) { - logger.error('[SQLite] Database close failed:', error); - throw new Error('Failed to close database'); - } -} - -/** - * Cleanup inactive connections - */ -function startConnectionCleanup(): void { - setInterval(() => { - if (connection && Date.now() - connection.lastUsed > CONNECTION_TIMEOUT) { - closeDatabase().catch(error => { - logger.error('[SQLite] Connection cleanup failed:', error); - }); - } - }, 60000); // Check every minute -} - -// ============================================================================ -// Schema Management -// ============================================================================ - -/** - * Create the database schema - */ -export async function createSchema(): Promise { - const { db } = await initDatabase(); - - try { - await db.transaction(async () => { - for (const table of DATABASE_SCHEMA) { - await createTable(db, table); - } - }); - } catch (error) { - logger.error('[SQLite] Schema creation failed:', error); - throw new Error('Failed to create database schema'); - } -} - -/** - * Create a single table - */ -async function createTable(db: Database, table: SQLiteTable): Promise { - const columnDefs = table.columns.map(col => { - const constraints = [ - col.primaryKey ? 'PRIMARY KEY' : '', - col.unique ? 'UNIQUE' : '', - !col.nullable ? 'NOT NULL' : '', - col.references ? `REFERENCES ${col.references.table}(${col.references.column})` : '', - col.default !== undefined ? `DEFAULT ${formatDefaultValue(col.default)}` : '' - ].filter(Boolean).join(' '); - - return `${col.name} ${col.type} ${constraints}`.trim(); - }); - - const createTableSQL = ` - CREATE TABLE IF NOT EXISTS ${table.name} ( - ${columnDefs.join(',\n ')} - ); - `; - - await db.exec(createTableSQL); - - // Create indexes - if (table.indexes) { - for (const index of table.indexes) { - const createIndexSQL = ` - CREATE INDEX IF NOT EXISTS ${index.name} - ON ${table.name} (${index.columns.join(', ')}) - ${index.unique ? 'UNIQUE' : ''}; - `; - await db.exec(createIndexSQL); - } - } -} - -/** - * Format default value for SQL - */ -function formatDefaultValue(value: unknown): string { - if (value === null) return 'NULL'; - if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`; - if (typeof value === 'number') return value.toString(); - if (typeof value === 'boolean') return value ? '1' : '0'; - throw new Error(`Unsupported default value type: ${typeof value}`); -} - -// ============================================================================ -// Database Health Checks -// ============================================================================ - -/** - * Check database health - */ -export async function checkDatabaseHealth(): Promise<{ - isHealthy: boolean; - tables: string[]; - error?: string; -}> { - try { - const { db } = await initDatabase(); - - // Check if we can query the database - const tables = await db.selectAll<{ name: string }>(` - SELECT name FROM sqlite_master - WHERE type='table' AND name NOT LIKE 'sqlite_%' - `); - - return { - isHealthy: true, - tables: tables.map(t => t.name) - }; - } catch (error) { - logger.error('[SQLite] Health check failed:', error); - return { - isHealthy: false, - tables: [], - error: error instanceof Error ? error.message : 'Unknown error' - }; - } -} - -/** - * Verify database integrity - */ -export async function verifyDatabaseIntegrity(): Promise<{ - isIntegrityOk: boolean; - errors: string[]; -}> { - const { db } = await initDatabase(); - const errors: string[] = []; - - try { - // Run integrity check - const result = await db.selectAll<{ integrity_check: string }>('PRAGMA integrity_check;'); - - if (result[0]?.integrity_check !== 'ok') { - errors.push('Database integrity check failed'); - } - - // Check foreign key constraints - const fkResult = await db.selectAll<{ table: string; rowid: number; parent: string; fkid: number }>(` - PRAGMA foreign_key_check; - `); - - if (fkResult.length > 0) { - errors.push('Foreign key constraint violations found'); - } - - return { - isIntegrityOk: errors.length === 0, - errors - }; - } catch (error) { - logger.error('[SQLite] Integrity check failed:', error); - return { - isIntegrityOk: false, - errors: [error instanceof Error ? error.message : 'Unknown error'] - }; - } -} - -// ============================================================================ -// Database Backup and Recovery -// ============================================================================ - -/** - * Create a database backup - */ -export async function createBackup(): Promise { - const { db } = await initDatabase(); - - try { - // Export the database to a binary array - return await db.export(); - } catch (error) { - logger.error('[SQLite] Backup creation failed:', error); - throw new Error('Failed to create database backup'); - } -} - -/** - * Restore database from backup - */ -export async function restoreFromBackup(backup: Uint8Array): Promise { - const { db } = await initDatabase(); - - try { - // Close current connection - await closeDatabase(); - - // Create new connection and import backup - const sqlite3 = await import('@wa-sqlite/sql.js'); - const newDb = await sqlite3.open(backup); - - // Verify integrity - const { isIntegrityOk, errors } = await verifyDatabaseIntegrity(); - if (!isIntegrityOk) { - throw new Error(`Backup integrity check failed: ${errors.join(', ')}`); - } - - // Replace current connection - connection = { - db: newDb, - sqlite3, - isOpen: true, - lastUsed: Date.now() - }; - } catch (error) { - logger.error('[SQLite] Backup restoration failed:', error); - throw new Error('Failed to restore database from backup'); - } -} \ No newline at end of file diff --git a/src/db/sqlite/migration.ts b/src/db/sqlite/migration.ts deleted file mode 100644 index 7a38dba6..00000000 --- a/src/db/sqlite/migration.ts +++ /dev/null @@ -1,374 +0,0 @@ -/** - * SQLite Migration Utilities - * - * This module handles the migration of data from Dexie to SQLite, - * including data transformation, validation, and rollback capabilities. - */ - -import { Database } from '@wa-sqlite/sql.js'; -import { initDatabase, createSchema, createBackup } from './init'; -import { - MigrationData, - MigrationResult, - SQLiteAccount, - SQLiteContact, - SQLiteContactMethod, - SQLiteSettings, - SQLiteLog, - SQLiteSecret, - isSQLiteAccount, - isSQLiteContact, - isSQLiteSettings -} from './types'; -import { logger } from '../../utils/logger'; - -// ============================================================================ -// Migration Types -// ============================================================================ - -interface MigrationContext { - db: Database; - startTime: number; - stats: MigrationResult['stats']; - errors: Error[]; -} - -// ============================================================================ -// Migration Functions -// ============================================================================ - -/** - * Migrate data from Dexie to SQLite - */ -export async function migrateFromDexie(data: MigrationData): Promise { - const startTime = Date.now(); - const context: MigrationContext = { - db: (await initDatabase()).db, - startTime, - stats: { - accounts: 0, - contacts: 0, - contactMethods: 0, - settings: 0, - logs: 0, - secrets: 0 - }, - errors: [] - }; - - try { - // Create backup before migration - const backup = await createBackup(); - - // Create schema if needed - await createSchema(); - - // Perform migration in a transaction - await context.db.transaction(async () => { - // Migrate in order of dependencies - await migrateAccounts(context, data.accounts); - await migrateContacts(context, data.contacts); - await migrateContactMethods(context, data.contactMethods); - await migrateSettings(context, data.settings); - await migrateLogs(context, data.logs); - await migrateSecrets(context, data.secrets); - }); - - // Verify migration - const verificationResult = await verifyMigration(context, data); - if (!verificationResult.success) { - throw new Error(`Migration verification failed: ${verificationResult.error}`); - } - - return { - success: true, - stats: context.stats, - duration: Date.now() - startTime - }; - - } catch (error) { - logger.error('[SQLite] Migration failed:', error); - - // Attempt rollback - try { - await rollbackMigration(backup); - } catch (rollbackError) { - logger.error('[SQLite] Rollback failed:', rollbackError); - context.errors.push(new Error('Migration and rollback failed')); - } - - return { - success: false, - error: error instanceof Error ? error : new Error('Unknown migration error'), - stats: context.stats, - duration: Date.now() - startTime - }; - } -} - -// ============================================================================ -// Migration Helpers -// ============================================================================ - -/** - * Migrate accounts - */ -async function migrateAccounts(context: MigrationContext, accounts: SQLiteAccount[]): Promise { - for (const account of accounts) { - try { - if (!isSQLiteAccount(account)) { - throw new Error(`Invalid account data: ${JSON.stringify(account)}`); - } - - await context.db.exec(` - INSERT INTO accounts ( - did, public_key_hex, created_at, updated_at, - identity_json, mnemonic_encrypted, passkey_cred_id_hex, derivation_path - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, [ - account.did, - account.public_key_hex, - account.created_at, - account.updated_at, - account.identity_json || null, - account.mnemonic_encrypted || null, - account.passkey_cred_id_hex || null, - account.derivation_path || null - ]); - - context.stats.accounts++; - } catch (error) { - context.errors.push(new Error(`Failed to migrate account ${account.did}: ${error}`)); - throw error; // Re-throw to trigger transaction rollback - } - } -} - -/** - * Migrate contacts - */ -async function migrateContacts(context: MigrationContext, contacts: SQLiteContact[]): Promise { - for (const contact of contacts) { - try { - if (!isSQLiteContact(contact)) { - throw new Error(`Invalid contact data: ${JSON.stringify(contact)}`); - } - - await context.db.exec(` - INSERT INTO contacts ( - id, did, name, notes, profile_image_url, - public_key_base64, next_pub_key_hash_b64, - sees_me, registered, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - contact.id, - contact.did, - contact.name || null, - contact.notes || null, - contact.profile_image_url || null, - contact.public_key_base64 || null, - contact.next_pub_key_hash_b64 || null, - contact.sees_me ? 1 : 0, - contact.registered ? 1 : 0, - contact.created_at, - contact.updated_at - ]); - - context.stats.contacts++; - } catch (error) { - context.errors.push(new Error(`Failed to migrate contact ${contact.id}: ${error}`)); - throw error; - } - } -} - -/** - * Migrate contact methods - */ -async function migrateContactMethods( - context: MigrationContext, - methods: SQLiteContactMethod[] -): Promise { - for (const method of methods) { - try { - await context.db.exec(` - INSERT INTO contact_methods ( - id, contact_id, label, type, value, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `, [ - method.id, - method.contact_id, - method.label, - method.type, - method.value, - method.created_at, - method.updated_at - ]); - - context.stats.contactMethods++; - } catch (error) { - context.errors.push(new Error(`Failed to migrate contact method ${method.id}: ${error}`)); - throw error; - } - } -} - -/** - * Migrate settings - */ -async function migrateSettings(context: MigrationContext, settings: SQLiteSettings[]): Promise { - for (const setting of settings) { - try { - if (!isSQLiteSettings(setting)) { - throw new Error(`Invalid settings data: ${JSON.stringify(setting)}`); - } - - await context.db.exec(` - INSERT INTO settings ( - key, account_did, value_json, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?) - `, [ - setting.key, - setting.account_did || null, - setting.value_json, - setting.created_at, - setting.updated_at - ]); - - context.stats.settings++; - } catch (error) { - context.errors.push(new Error(`Failed to migrate setting ${setting.key}: ${error}`)); - throw error; - } - } -} - -/** - * Migrate logs - */ -async function migrateLogs(context: MigrationContext, logs: SQLiteLog[]): Promise { - for (const log of logs) { - try { - await context.db.exec(` - INSERT INTO logs ( - id, level, message, metadata_json, created_at - ) VALUES (?, ?, ?, ?, ?) - `, [ - log.id, - log.level, - log.message, - log.metadata_json || null, - log.created_at - ]); - - context.stats.logs++; - } catch (error) { - context.errors.push(new Error(`Failed to migrate log ${log.id}: ${error}`)); - throw error; - } - } -} - -/** - * Migrate secrets - */ -async function migrateSecrets(context: MigrationContext, secrets: SQLiteSecret[]): Promise { - for (const secret of secrets) { - try { - await context.db.exec(` - INSERT INTO secrets ( - key, value_encrypted, created_at, updated_at - ) VALUES (?, ?, ?, ?) - `, [ - secret.key, - secret.value_encrypted, - secret.created_at, - secret.updated_at - ]); - - context.stats.secrets++; - } catch (error) { - context.errors.push(new Error(`Failed to migrate secret ${secret.key}: ${error}`)); - throw error; - } - } -} - -// ============================================================================ -// Verification and Rollback -// ============================================================================ - -/** - * Verify migration success - */ -async function verifyMigration( - context: MigrationContext, - data: MigrationData -): Promise<{ success: boolean; error?: string }> { - try { - // Verify counts - const counts = await context.db.selectAll<{ table: string; count: number }>(` - SELECT 'accounts' as table, COUNT(*) as count FROM accounts - UNION ALL - SELECT 'contacts', COUNT(*) FROM contacts - UNION ALL - SELECT 'contact_methods', COUNT(*) FROM contact_methods - UNION ALL - SELECT 'settings', COUNT(*) FROM settings - UNION ALL - SELECT 'logs', COUNT(*) FROM logs - UNION ALL - SELECT 'secrets', COUNT(*) FROM secrets - `); - - const countMap = new Map(counts.map(c => [c.table, c.count])); - - if (countMap.get('accounts') !== data.accounts.length) { - return { success: false, error: 'Account count mismatch' }; - } - if (countMap.get('contacts') !== data.contacts.length) { - return { success: false, error: 'Contact count mismatch' }; - } - if (countMap.get('contact_methods') !== data.contactMethods.length) { - return { success: false, error: 'Contact method count mismatch' }; - } - if (countMap.get('settings') !== data.settings.length) { - return { success: false, error: 'Settings count mismatch' }; - } - if (countMap.get('logs') !== data.logs.length) { - return { success: false, error: 'Log count mismatch' }; - } - if (countMap.get('secrets') !== data.secrets.length) { - return { success: false, error: 'Secret count mismatch' }; - } - - return { success: true }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown verification error' - }; - } -} - -/** - * Rollback migration - */ -async function rollbackMigration(backup: Uint8Array): Promise { - const { db } = await initDatabase(); - - try { - // Close current connection - await db.close(); - - // Restore from backup - const sqlite3 = await import('@wa-sqlite/sql.js'); - await sqlite3.open(backup); - - logger.info('[SQLite] Migration rollback successful'); - } catch (error) { - logger.error('[SQLite] Migration rollback failed:', error); - throw new Error('Failed to rollback migration'); - } -} \ No newline at end of file diff --git a/src/db/sqlite/operations.ts b/src/db/sqlite/operations.ts deleted file mode 100644 index ebb6511a..00000000 --- a/src/db/sqlite/operations.ts +++ /dev/null @@ -1,449 +0,0 @@ -/** - * SQLite Database Operations - * - * This module provides utility functions for common database operations, - * including CRUD operations, queries, and transactions. - */ - -import { Database } from '@wa-sqlite/sql.js'; -import { initDatabase } from './init'; -import { - SQLiteAccount, - SQLiteContact, - SQLiteContactMethod, - SQLiteSettings, - SQLiteLog, - SQLiteSecret, - isSQLiteAccount, - isSQLiteContact, - isSQLiteSettings -} from './types'; -import { logger } from '../../utils/logger'; - -// ============================================================================ -// Transaction Helpers -// ============================================================================ - -/** - * Execute a function within a transaction - */ -export async function withTransaction( - operation: (db: Database) => Promise -): Promise { - const { db } = await initDatabase(); - - try { - return await db.transaction(operation); - } catch (error) { - logger.error('[SQLite] Transaction failed:', error); - throw error; - } -} - -/** - * Execute a function with retries - */ -export async function withRetry( - operation: () => Promise, - maxRetries = 3, - delay = 1000 -): Promise { - let lastError: Error | undefined; - - for (let i = 0; i < maxRetries; i++) { - try { - return await operation(); - } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)); - - if (i < maxRetries - 1) { - await new Promise(resolve => setTimeout(resolve, delay * (i + 1))); - } - } - } - - throw lastError; -} - -// ============================================================================ -// Account Operations -// ============================================================================ - -/** - * Get account by DID - */ -export async function getAccountByDid(did: string): Promise { - const { db } = await initDatabase(); - - const accounts = await db.selectAll( - 'SELECT * FROM accounts WHERE did = ?', - [did] - ); - - return accounts[0] || null; -} - -/** - * Get all accounts - */ -export async function getAllAccounts(): Promise { - const { db } = await initDatabase(); - - return db.selectAll( - 'SELECT * FROM accounts ORDER BY created_at DESC' - ); -} - -/** - * Create or update account - */ -export async function upsertAccount(account: SQLiteAccount): Promise { - if (!isSQLiteAccount(account)) { - throw new Error('Invalid account data'); - } - - await withTransaction(async (db) => { - const existing = await db.selectOne<{ did: string }>( - 'SELECT did FROM accounts WHERE did = ?', - [account.did] - ); - - if (existing) { - await db.exec(` - UPDATE accounts SET - public_key_hex = ?, - updated_at = ?, - identity_json = ?, - mnemonic_encrypted = ?, - passkey_cred_id_hex = ?, - derivation_path = ? - WHERE did = ? - `, [ - account.public_key_hex, - Date.now(), - account.identity_json || null, - account.mnemonic_encrypted || null, - account.passkey_cred_id_hex || null, - account.derivation_path || null, - account.did - ]); - } else { - await db.exec(` - INSERT INTO accounts ( - did, public_key_hex, created_at, updated_at, - identity_json, mnemonic_encrypted, passkey_cred_id_hex, derivation_path - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, [ - account.did, - account.public_key_hex, - account.created_at, - account.updated_at, - account.identity_json || null, - account.mnemonic_encrypted || null, - account.passkey_cred_id_hex || null, - account.derivation_path || null - ]); - } - }); -} - -// ============================================================================ -// Contact Operations -// ============================================================================ - -/** - * Get contact by ID - */ -export async function getContactById(id: string): Promise { - const { db } = await initDatabase(); - - const contacts = await db.selectAll( - 'SELECT * FROM contacts WHERE id = ?', - [id] - ); - - return contacts[0] || null; -} - -/** - * Get contacts by account DID - */ -export async function getContactsByAccountDid(did: string): Promise { - const { db } = await initDatabase(); - - return db.selectAll( - 'SELECT * FROM contacts WHERE did = ? ORDER BY created_at DESC', - [did] - ); -} - -/** - * Get contact methods for a contact - */ -export async function getContactMethods(contactId: string): Promise { - const { db } = await initDatabase(); - - return db.selectAll( - 'SELECT * FROM contact_methods WHERE contact_id = ? ORDER BY created_at DESC', - [contactId] - ); -} - -/** - * Create or update contact with methods - */ -export async function upsertContact( - contact: SQLiteContact, - methods: SQLiteContactMethod[] = [] -): Promise { - if (!isSQLiteContact(contact)) { - throw new Error('Invalid contact data'); - } - - await withTransaction(async (db) => { - const existing = await db.selectOne<{ id: string }>( - 'SELECT id FROM contacts WHERE id = ?', - [contact.id] - ); - - if (existing) { - await db.exec(` - UPDATE contacts SET - did = ?, - name = ?, - notes = ?, - profile_image_url = ?, - public_key_base64 = ?, - next_pub_key_hash_b64 = ?, - sees_me = ?, - registered = ?, - updated_at = ? - WHERE id = ? - `, [ - contact.did, - contact.name || null, - contact.notes || null, - contact.profile_image_url || null, - contact.public_key_base64 || null, - contact.next_pub_key_hash_b64 || null, - contact.sees_me ? 1 : 0, - contact.registered ? 1 : 0, - Date.now(), - contact.id - ]); - } else { - await db.exec(` - INSERT INTO contacts ( - id, did, name, notes, profile_image_url, - public_key_base64, next_pub_key_hash_b64, - sees_me, registered, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - contact.id, - contact.did, - contact.name || null, - contact.notes || null, - contact.profile_image_url || null, - contact.public_key_base64 || null, - contact.next_pub_key_hash_b64 || null, - contact.sees_me ? 1 : 0, - contact.registered ? 1 : 0, - contact.created_at, - contact.updated_at - ]); - } - - // Update contact methods - if (methods.length > 0) { - // Delete existing methods - await db.exec( - 'DELETE FROM contact_methods WHERE contact_id = ?', - [contact.id] - ); - - // Insert new methods - for (const method of methods) { - await db.exec(` - INSERT INTO contact_methods ( - id, contact_id, label, type, value, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `, [ - method.id, - contact.id, - method.label, - method.type, - method.value, - method.created_at, - method.updated_at - ]); - } - } - }); -} - -// ============================================================================ -// Settings Operations -// ============================================================================ - -/** - * Get setting by key - */ -export async function getSetting(key: string): Promise { - const { db } = await initDatabase(); - - const settings = await db.selectAll( - 'SELECT * FROM settings WHERE key = ?', - [key] - ); - - return settings[0] || null; -} - -/** - * Get settings by account DID - */ -export async function getSettingsByAccountDid(did: string): Promise { - const { db } = await initDatabase(); - - return db.selectAll( - 'SELECT * FROM settings WHERE account_did = ? ORDER BY updated_at DESC', - [did] - ); -} - -/** - * Set setting value - */ -export async function setSetting(setting: SQLiteSettings): Promise { - if (!isSQLiteSettings(setting)) { - throw new Error('Invalid settings data'); - } - - await withTransaction(async (db) => { - const existing = await db.selectOne<{ key: string }>( - 'SELECT key FROM settings WHERE key = ?', - [setting.key] - ); - - if (existing) { - await db.exec(` - UPDATE settings SET - account_did = ?, - value_json = ?, - updated_at = ? - WHERE key = ? - `, [ - setting.account_did || null, - setting.value_json, - Date.now(), - setting.key - ]); - } else { - await db.exec(` - INSERT INTO settings ( - key, account_did, value_json, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?) - `, [ - setting.key, - setting.account_did || null, - setting.value_json, - setting.created_at, - setting.updated_at - ]); - } - }); -} - -// ============================================================================ -// Log Operations -// ============================================================================ - -/** - * Add log entry - */ -export async function addLog(log: SQLiteLog): Promise { - await withTransaction(async (db) => { - await db.exec(` - INSERT INTO logs ( - id, level, message, metadata_json, created_at - ) VALUES (?, ?, ?, ?, ?) - `, [ - log.id, - log.level, - log.message, - log.metadata_json || null, - log.created_at - ]); - }); -} - -/** - * Get logs by level - */ -export async function getLogsByLevel( - level: string, - limit = 100, - offset = 0 -): Promise { - const { db } = await initDatabase(); - - return db.selectAll( - 'SELECT * FROM logs WHERE level = ? ORDER BY created_at DESC LIMIT ? OFFSET ?', - [level, limit, offset] - ); -} - -// ============================================================================ -// Secret Operations -// ============================================================================ - -/** - * Get secret by key - */ -export async function getSecret(key: string): Promise { - const { db } = await initDatabase(); - - const secrets = await db.selectAll( - 'SELECT * FROM secrets WHERE key = ?', - [key] - ); - - return secrets[0] || null; -} - -/** - * Set secret value - */ -export async function setSecret(secret: SQLiteSecret): Promise { - await withTransaction(async (db) => { - const existing = await db.selectOne<{ key: string }>( - 'SELECT key FROM secrets WHERE key = ?', - [secret.key] - ); - - if (existing) { - await db.exec(` - UPDATE secrets SET - value_encrypted = ?, - updated_at = ? - WHERE key = ? - `, [ - secret.value_encrypted, - Date.now(), - secret.key - ]); - } else { - await db.exec(` - INSERT INTO secrets ( - key, value_encrypted, created_at, updated_at - ) VALUES (?, ?, ?, ?) - `, [ - secret.key, - secret.value_encrypted, - secret.created_at, - secret.updated_at - ]); - } - }); -} \ No newline at end of file diff --git a/src/db/sqlite/types.ts b/src/db/sqlite/types.ts deleted file mode 100644 index 4cfcab74..00000000 --- a/src/db/sqlite/types.ts +++ /dev/null @@ -1,349 +0,0 @@ -/** - * SQLite Type Definitions - * - * This file defines the type system for the SQLite implementation, - * mapping from the existing Dexie types to SQLite-compatible types. - * It includes both the database schema types and the runtime types. - */ - -import { SQLiteCompatibleType } from '@jlongster/sql.js'; - -// ============================================================================ -// Base Types and Utilities -// ============================================================================ - -/** - * SQLite column type mapping - */ -export type SQLiteColumnType = - | 'INTEGER' // For numbers, booleans, dates - | 'TEXT' // For strings, JSON - | 'BLOB' // For binary data - | 'REAL' // For floating point numbers - | 'NULL'; // For null values - -/** - * SQLite column definition - */ -export interface SQLiteColumn { - name: string; - type: SQLiteColumnType; - nullable?: boolean; - primaryKey?: boolean; - unique?: boolean; - references?: { - table: string; - column: string; - }; - default?: SQLiteCompatibleType; -} - -/** - * SQLite table definition - */ -export interface SQLiteTable { - name: string; - columns: SQLiteColumn[]; - indexes?: Array<{ - name: string; - columns: string[]; - unique?: boolean; - }>; -} - -// ============================================================================ -// Account Types -// ============================================================================ - -/** - * SQLite-compatible Account type - * Maps from the Dexie Account type - */ -export interface SQLiteAccount { - did: string; // TEXT PRIMARY KEY - public_key_hex: string; // TEXT NOT NULL - created_at: number; // INTEGER NOT NULL - updated_at: number; // INTEGER NOT NULL - identity_json?: string; // TEXT (encrypted JSON) - mnemonic_encrypted?: string; // TEXT (encrypted) - passkey_cred_id_hex?: string; // TEXT - derivation_path?: string; // TEXT -} - -export const ACCOUNTS_TABLE: SQLiteTable = { - name: 'accounts', - columns: [ - { name: 'did', type: 'TEXT', primaryKey: true }, - { name: 'public_key_hex', type: 'TEXT', nullable: false }, - { name: 'created_at', type: 'INTEGER', nullable: false }, - { name: 'updated_at', type: 'INTEGER', nullable: false }, - { name: 'identity_json', type: 'TEXT' }, - { name: 'mnemonic_encrypted', type: 'TEXT' }, - { name: 'passkey_cred_id_hex', type: 'TEXT' }, - { name: 'derivation_path', type: 'TEXT' } - ], - indexes: [ - { name: 'idx_accounts_created_at', columns: ['created_at'] }, - { name: 'idx_accounts_updated_at', columns: ['updated_at'] } - ] -}; - -// ============================================================================ -// Contact Types -// ============================================================================ - -/** - * SQLite-compatible ContactMethod type - */ -export interface SQLiteContactMethod { - id: string; // TEXT PRIMARY KEY - contact_id: string; // TEXT NOT NULL - label: string; // TEXT NOT NULL - type: string; // TEXT NOT NULL - value: string; // TEXT NOT NULL - created_at: number; // INTEGER NOT NULL - updated_at: number; // INTEGER NOT NULL -} - -/** - * SQLite-compatible Contact type - */ -export interface SQLiteContact { - id: string; // TEXT PRIMARY KEY - did: string; // TEXT NOT NULL - name?: string; // TEXT - notes?: string; // TEXT - profile_image_url?: string; // TEXT - public_key_base64?: string; // TEXT - next_pub_key_hash_b64?: string; // TEXT - sees_me?: boolean; // INTEGER (0 or 1) - registered?: boolean; // INTEGER (0 or 1) - created_at: number; // INTEGER NOT NULL - updated_at: number; // INTEGER NOT NULL -} - -export const CONTACTS_TABLE: SQLiteTable = { - name: 'contacts', - columns: [ - { name: 'id', type: 'TEXT', primaryKey: true }, - { name: 'did', type: 'TEXT', nullable: false }, - { name: 'name', type: 'TEXT' }, - { name: 'notes', type: 'TEXT' }, - { name: 'profile_image_url', type: 'TEXT' }, - { name: 'public_key_base64', type: 'TEXT' }, - { name: 'next_pub_key_hash_b64', type: 'TEXT' }, - { name: 'sees_me', type: 'INTEGER' }, - { name: 'registered', type: 'INTEGER' }, - { name: 'created_at', type: 'INTEGER', nullable: false }, - { name: 'updated_at', type: 'INTEGER', nullable: false } - ], - indexes: [ - { name: 'idx_contacts_did', columns: ['did'] }, - { name: 'idx_contacts_created_at', columns: ['created_at'] } - ] -}; - -export const CONTACT_METHODS_TABLE: SQLiteTable = { - name: 'contact_methods', - columns: [ - { name: 'id', type: 'TEXT', primaryKey: true }, - { name: 'contact_id', type: 'TEXT', nullable: false, - references: { table: 'contacts', column: 'id' } }, - { name: 'label', type: 'TEXT', nullable: false }, - { name: 'type', type: 'TEXT', nullable: false }, - { name: 'value', type: 'TEXT', nullable: false }, - { name: 'created_at', type: 'INTEGER', nullable: false }, - { name: 'updated_at', type: 'INTEGER', nullable: false } - ], - indexes: [ - { name: 'idx_contact_methods_contact_id', columns: ['contact_id'] } - ] -}; - -// ============================================================================ -// Settings Types -// ============================================================================ - -/** - * SQLite-compatible Settings type - */ -export interface SQLiteSettings { - key: string; // TEXT PRIMARY KEY - account_did?: string; // TEXT - value_json: string; // TEXT NOT NULL (JSON stringified) - created_at: number; // INTEGER NOT NULL - updated_at: number; // INTEGER NOT NULL -} - -export const SETTINGS_TABLE: SQLiteTable = { - name: 'settings', - columns: [ - { name: 'key', type: 'TEXT', primaryKey: true }, - { name: 'account_did', type: 'TEXT' }, - { name: 'value_json', type: 'TEXT', nullable: false }, - { name: 'created_at', type: 'INTEGER', nullable: false }, - { name: 'updated_at', type: 'INTEGER', nullable: false } - ], - indexes: [ - { name: 'idx_settings_account_did', columns: ['account_did'] }, - { name: 'idx_settings_updated_at', columns: ['updated_at'] } - ] -}; - -// ============================================================================ -// Log Types -// ============================================================================ - -/** - * SQLite-compatible Log type - */ -export interface SQLiteLog { - id: string; // TEXT PRIMARY KEY - level: string; // TEXT NOT NULL - message: string; // TEXT NOT NULL - metadata_json?: string; // TEXT (JSON stringified) - created_at: number; // INTEGER NOT NULL -} - -export const LOGS_TABLE: SQLiteTable = { - name: 'logs', - columns: [ - { name: 'id', type: 'TEXT', primaryKey: true }, - { name: 'level', type: 'TEXT', nullable: false }, - { name: 'message', type: 'TEXT', nullable: false }, - { name: 'metadata_json', type: 'TEXT' }, - { name: 'created_at', type: 'INTEGER', nullable: false } - ], - indexes: [ - { name: 'idx_logs_level', columns: ['level'] }, - { name: 'idx_logs_created_at', columns: ['created_at'] } - ] -}; - -// ============================================================================ -// Secret Types -// ============================================================================ - -/** - * SQLite-compatible Secret type - * Note: This table should be encrypted at the database level - */ -export interface SQLiteSecret { - key: string; // TEXT PRIMARY KEY - value_encrypted: string; // TEXT NOT NULL (encrypted) - created_at: number; // INTEGER NOT NULL - updated_at: number; // INTEGER NOT NULL -} - -export const SECRETS_TABLE: SQLiteTable = { - name: 'secrets', - columns: [ - { name: 'key', type: 'TEXT', primaryKey: true }, - { name: 'value_encrypted', type: 'TEXT', nullable: false }, - { name: 'created_at', type: 'INTEGER', nullable: false }, - { name: 'updated_at', type: 'INTEGER', nullable: false } - ], - indexes: [ - { name: 'idx_secrets_updated_at', columns: ['updated_at'] } - ] -}; - -// ============================================================================ -// Database Schema -// ============================================================================ - -/** - * Complete database schema definition - */ -export const DATABASE_SCHEMA: SQLiteTable[] = [ - ACCOUNTS_TABLE, - CONTACTS_TABLE, - CONTACT_METHODS_TABLE, - SETTINGS_TABLE, - LOGS_TABLE, - SECRETS_TABLE -]; - -// ============================================================================ -// Type Guards and Validators -// ============================================================================ - -/** - * Type guard for SQLiteAccount - */ -export function isSQLiteAccount(value: unknown): value is SQLiteAccount { - return ( - typeof value === 'object' && - value !== null && - typeof (value as SQLiteAccount).did === 'string' && - typeof (value as SQLiteAccount).public_key_hex === 'string' && - typeof (value as SQLiteAccount).created_at === 'number' && - typeof (value as SQLiteAccount).updated_at === 'number' - ); -} - -/** - * Type guard for SQLiteContact - */ -export function isSQLiteContact(value: unknown): value is SQLiteContact { - return ( - typeof value === 'object' && - value !== null && - typeof (value as SQLiteContact).id === 'string' && - typeof (value as SQLiteContact).did === 'string' && - typeof (value as SQLiteContact).created_at === 'number' && - typeof (value as SQLiteContact).updated_at === 'number' - ); -} - -/** - * Type guard for SQLiteSettings - */ -export function isSQLiteSettings(value: unknown): value is SQLiteSettings { - return ( - typeof value === 'object' && - value !== null && - typeof (value as SQLiteSettings).key === 'string' && - typeof (value as SQLiteSettings).value_json === 'string' && - typeof (value as SQLiteSettings).created_at === 'number' && - typeof (value as SQLiteSettings).updated_at === 'number' - ); -} - -// ============================================================================ -// Migration Types -// ============================================================================ - -/** - * Type for migration data from Dexie to SQLite - */ -export interface MigrationData { - accounts: SQLiteAccount[]; - contacts: SQLiteContact[]; - contactMethods: SQLiteContactMethod[]; - settings: SQLiteSettings[]; - logs: SQLiteLog[]; - secrets: SQLiteSecret[]; - metadata: { - version: string; - timestamp: number; - source: 'dexie'; - }; -} - -/** - * Migration result type - */ -export interface MigrationResult { - success: boolean; - error?: Error; - stats: { - accounts: number; - contacts: number; - contactMethods: number; - settings: number; - logs: number; - secrets: number; - }; - duration: number; -} \ No newline at end of file diff --git a/src/interfaces/database.ts b/src/interfaces/database.ts index 0e024c55..4dda80ef 100644 --- a/src/interfaces/database.ts +++ b/src/interfaces/database.ts @@ -7,11 +7,11 @@ export interface QueryExecResult { export interface DatabaseService { initialize(): Promise; - query(sql: string, params?: any[]): Promise; + query(sql: string, params?: unknown[]): Promise; run( sql: string, - params?: any[], + params?: unknown[], ): Promise<{ changes: number; lastId?: number }>; - getOneRow(sql: string, params?: any[]): Promise; - getAll(sql: string, params?: any[]): Promise; + getOneRow(sql: string, params?: unknown[]): Promise; + getAll(sql: string, params?: unknown[]): Promise; } diff --git a/src/libs/util.ts b/src/libs/util.ts index 51b1f063..bd70fb72 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -567,7 +567,7 @@ export const generateSaveAndActivateIdentity = async (): Promise => { await updateDefaultSettings({ activeDid: newId.did }); } catch (error) { - console.error("Failed to update default settings:", error); + logger.error("Failed to update default settings:", error); throw new Error( "Failed to set default settings. Please try again or restart the app.", ); diff --git a/src/services/database.d.ts b/src/services/database.d.ts index 08032bfd..3741898e 100644 --- a/src/services/database.d.ts +++ b/src/services/database.d.ts @@ -2,9 +2,9 @@ import { DatabaseService } from "../interfaces/database"; declare module "@jlongster/sql.js" { interface SQL { - Database: any; - FS: any; - register_for_idb: (fs: any) => void; + Database: unknown; + FS: unknown; + register_for_idb: (fs: unknown) => void; } function initSqlJs(config: { @@ -15,7 +15,7 @@ declare module "@jlongster/sql.js" { declare module "absurd-sql" { export class SQLiteFS { - constructor(fs: any, backend: any); + constructor(fs: unknown, backend: unknown); } } diff --git a/src/services/database.ts b/src/services/database.ts index 907a0e80..40adbd1c 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -9,12 +9,13 @@ import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend"; import { runMigrations } from "../db-sql/migration"; import type { QueryExecResult } from "../interfaces/database"; +import { logger } from "@/utils/logger"; interface SQLDatabase { - exec: (sql: string, params?: any[]) => Promise; + exec: (sql: string, params?: unknown[]) => Promise; run: ( sql: string, - params?: any[], + params?: unknown[], ) => Promise<{ changes: number; lastId?: number }>; } @@ -52,7 +53,7 @@ class DatabaseService { try { await this.initializationPromise; } catch (error) { - console.error(`DatabaseService initialize method failed:`, error); + logger.error(`DatabaseService initialize method failed:`, error); this.initializationPromise = null; // Reset on failure throw error; } @@ -116,7 +117,7 @@ class DatabaseService { // If initialized but no db, something went wrong if (!this.db) { - console.error( + logger.error( `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`, ); throw new Error( @@ -128,25 +129,28 @@ class DatabaseService { // Used for inserts, updates, and deletes async run( sql: string, - params: any[] = [], + params: unknown[] = [], ): Promise<{ changes: number; lastId?: number }> { await this.waitForInitialization(); return this.db!.run(sql, params); } // Note that the resulting array may be empty if there are no results from the query - async query(sql: string, params: any[] = []): Promise { + async query(sql: string, params: unknown[] = []): Promise { await this.waitForInitialization(); return this.db!.exec(sql, params); } - async getOneRow(sql: string, params: any[] = []): Promise { + async getOneRow( + sql: string, + params: unknown[] = [], + ): Promise { await this.waitForInitialization(); const result = await this.db!.exec(sql, params); return result[0]?.values[0]; } - async all(sql: string, params: any[] = []): Promise { + async all(sql: string, params: unknown[] = []): Promise { await this.waitForInitialization(); const result = await this.db!.exec(sql, params); return result[0]?.values || []; diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 18f49b56..74d74244 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -1,3 +1,4 @@ +import logger from "@/utils/logger"; import { QueryExecResult } from "../interfaces/database"; interface Migration { @@ -23,7 +24,10 @@ export class MigrationService { } async runMigrations( - sqlExec: (sql: string, params?: any[]) => Promise>, + sqlExec: ( + sql: string, + params?: unknown[], + ) => Promise>, ): Promise { // Create migrations table if it doesn't exist await sqlExec(` @@ -43,7 +47,7 @@ export class MigrationService { if (result.length > 0) { const singleResult = result[0]; executedMigrations = new Set( - singleResult.values.map((row: any[]) => row[0]), + singleResult.values.map((row: unknown[]) => row[0]), ); } @@ -55,9 +59,9 @@ export class MigrationService { await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ migration.name, ]); - console.log(`Migration ${migration.name} executed successfully`); + logger.log(`Migration ${migration.name} executed successfully`); } catch (error) { - console.error(`Error executing migration ${migration.name}:`, error); + logger.error(`Error executing migration ${migration.name}:`, error); throw error; } } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index b3ae0d9e..cda77e1c 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -111,7 +111,10 @@ - + { this.loading = false; this.hitError = true; - console.error("Failed to generate identity:", error); + logger.error("Failed to generate identity:", error); }); } } diff --git a/src/views/TestView.vue b/src/views/TestView.vue index abf6d87e..98915fea 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -355,7 +355,7 @@ export default class Help extends Vue { // for SQL operations sqlQuery = ""; - sqlResult: any = null; + sqlResult: unknown = null; cryptoLib = cryptoLib; @@ -542,9 +542,9 @@ export default class Help extends Vue { } else { this.sqlResult = await databaseService.run(this.sqlQuery); } - console.log("SQL Result:", this.sqlResult); + logger.log("SQL Result:", this.sqlResult); } catch (error) { - console.error("SQL Error:", error); + logger.error("SQL Error:", error); this.$notify( { group: "alert",