diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index 4cf42653..8dae255c 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -175,10 +175,12 @@ export let memoryLogs: string[] = []; * @param level - The log level (error, warn, info, debug) * @author Matthew Raymer */ -export async function logToDb(message: string, level: string = "info"): Promise { +export async function logToDb( + message: string, + level: string = "info", +): Promise { const platform = PlatformServiceFactory.getInstance(); const todayKey = new Date().toDateString(); - const nowKey = new Date().toISOString(); try { memoryLogs.push(`${new Date().toISOString()} ${message}`); @@ -194,12 +196,8 @@ export async function logToDb(message: string, level: string = "info"): Promise< const sevenDaysAgo = new Date( new Date().getTime() - 7 * 24 * 60 * 60 * 1000, ).toDateString(); // Use date string to match schema - memoryLogs = memoryLogs.filter( - (log) => log.split(" ")[0] > sevenDaysAgo, - ); - await platform.dbExec("DELETE FROM logs WHERE date < ?", [ - sevenDaysAgo, - ]); + memoryLogs = memoryLogs.filter((log) => log.split(" ")[0] > sevenDaysAgo); + await platform.dbExec("DELETE FROM logs WHERE date < ?", [sevenDaysAgo]); lastCleanupDate = todayKey; } } catch (error) { diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index bd22115a..821fe186 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -1,23 +1,23 @@ /** * Database Migration Service for TimeSafari - * - * This module provides a comprehensive database migration system that manages + * + * This module provides a comprehensive database migration system that manages * schema changes as users upgrade their TimeSafari application over time. * The system ensures that database changes are applied safely, tracked properly, * and can handle edge cases gracefully. - * + * * ## Architecture Overview - * + * * The migration system follows these key principles: - * + * * 1. **Single Application**: Each migration runs exactly once per database * 2. **Tracked Execution**: All applied migrations are recorded in a migrations table * 3. **Schema Validation**: Actual database schema is validated before and after migrations * 4. **Graceful Recovery**: Handles cases where schema exists but tracking is missing * 5. **Comprehensive Logging**: Detailed logging for debugging and monitoring - * + * * ## Migration Flow - * + * * ``` * 1. Create migrations table (if needed) * 2. Query existing applied migrations @@ -30,28 +30,28 @@ * f. Record migration as applied * 4. Final validation of all migrations * ``` - * + * * ## Usage Example - * + * * ```typescript * // Register migrations (typically in migration.ts) * registerMigration({ * name: "001_initial", * sql: "CREATE TABLE accounts (id INTEGER PRIMARY KEY, ...)" * }); - * + * * // Run migrations (typically in platform service) * await runMigrations(sqlExec, sqlQuery, extractMigrationNames); * ``` - * + * * ## Error Handling - * + * * The system handles several error scenarios: * - Duplicate table/column errors (schema already exists) * - Migration tracking inconsistencies * - Database connection issues * - Schema validation failures - * + * * @author Matthew Raymer * @version 1.0.0 * @since 2025-06-30 @@ -61,11 +61,11 @@ import { logger } from "../utils/logger"; /** * Migration interface for database schema migrations - * + * * Represents a single database migration that can be applied to upgrade * the database schema. Each migration should be idempotent and focused * on a single schema change. - * + * * @interface Migration */ interface Migration { @@ -77,10 +77,10 @@ interface Migration { /** * Migration validation result - * + * * Contains the results of validating that a migration was successfully * applied by checking the actual database schema. - * + * * @interface MigrationValidation */ interface MigrationValidation { @@ -96,11 +96,11 @@ interface MigrationValidation { /** * Migration registry to store and manage database migrations - * + * * This class maintains a registry of all migrations that need to be applied * to the database. It uses the singleton pattern to ensure migrations are * registered once and can be accessed globally. - * + * * @class MigrationRegistry */ class MigrationRegistry { @@ -109,14 +109,14 @@ class MigrationRegistry { /** * Register a migration with the registry - * + * * Adds a migration to the list of migrations that will be applied when * runMigrations() is called. Migrations should be registered in order * of their intended execution. * * @param migration - The migration to register * @throws {Error} If migration name is empty or already exists - * + * * @example * ```typescript * registry.registerMigration({ @@ -126,14 +126,14 @@ class MigrationRegistry { * ``` */ registerMigration(migration: Migration): void { - if (!migration.name || migration.name.trim() === '') { - throw new Error('Migration name cannot be empty'); + if (!migration.name || migration.name.trim() === "") { + throw new Error("Migration name cannot be empty"); } - - if (this.migrations.some(m => m.name === migration.name)) { + + if (this.migrations.some((m) => m.name === migration.name)) { throw new Error(`Migration with name '${migration.name}' already exists`); } - + this.migrations.push(migration); } @@ -142,7 +142,7 @@ class MigrationRegistry { * * Returns a copy of all migrations that have been registered with this * registry. The migrations are returned in the order they were registered. - * + * * @returns Array of registered migrations (defensive copy) */ getMigrations(): Migration[] { @@ -151,10 +151,10 @@ class MigrationRegistry { /** * Clear all registered migrations - * + * * Removes all migrations from the registry. This is primarily used for * testing purposes to ensure a clean state between test runs. - * + * * @internal Used primarily for testing */ clearMigrations(): void { @@ -163,7 +163,7 @@ class MigrationRegistry { /** * Get the count of registered migrations - * + * * @returns Number of migrations currently registered */ getCount(): number { @@ -183,7 +183,7 @@ const migrationRegistry = new MigrationRegistry(); * * @param migration - The migration to register * @throws {Error} If migration is invalid - * + * * @example * ```typescript * registerMigration({ @@ -207,16 +207,16 @@ export function registerMigration(migration: Migration): void { /** * Validate that a migration was successfully applied by checking schema - * + * * This function performs post-migration validation to ensure that the * expected database schema changes were actually applied. It checks for * the existence of tables, columns, and other schema elements that should * have been created by the migration. - * + * * @param migration - The migration to validate * @param sqlQuery - Function to execute SQL queries * @returns Promise resolving to validation results - * + * * @example * ```typescript * const validation = await validateMigrationApplication(migration, sqlQuery); @@ -233,48 +233,68 @@ async function validateMigrationApplication( isValid: true, tableExists: false, hasExpectedColumns: false, - errors: [] + errors: [], }; try { if (migration.name === "001_initial") { // Validate core tables exist for initial migration - const tables = ['accounts', 'secret', 'settings', 'contacts', 'logs', 'temp']; - + const tables = [ + "accounts", + "secret", + "settings", + "contacts", + "logs", + "temp", + ]; + for (const tableName of tables) { try { - await sqlQuery(`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`); - console.log(`✅ [Migration-Validation] Table ${tableName} exists`); + await sqlQuery( + `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`, + ); + logger.log(`✅ [Migration-Validation] Table ${tableName} exists`); } catch (error) { validation.isValid = false; validation.errors.push(`Table ${tableName} missing`); - console.error(`❌ [Migration-Validation] Table ${tableName} missing:`, error); + logger.error( + `❌ [Migration-Validation] Table ${tableName} missing:`, + error, + ); } } validation.tableExists = validation.errors.length === 0; - } else if (migration.name === "002_add_iViewContent_to_contacts") { // Validate iViewContent column exists in contacts table try { await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`); validation.hasExpectedColumns = true; - console.log(`✅ [Migration-Validation] Column iViewContent exists in contacts table`); + logger.log( + `✅ [Migration-Validation] Column iViewContent exists in contacts table`, + ); } catch (error) { validation.isValid = false; - validation.errors.push(`Column iViewContent missing from contacts table`); - console.error(`❌ [Migration-Validation] Column iViewContent missing:`, error); + validation.errors.push( + `Column iViewContent missing from contacts table`, + ); + logger.error( + `❌ [Migration-Validation] Column iViewContent missing:`, + error, + ); } } - + // Add validation for future migrations here // } else if (migration.name === "003_future_migration") { // // Validate future migration schema changes // } - } catch (error) { validation.isValid = false; validation.errors.push(`Validation error: ${error}`); - console.error(`❌ [Migration-Validation] Validation failed for ${migration.name}:`, error); + logger.error( + `❌ [Migration-Validation] Validation failed for ${migration.name}:`, + error, + ); } return validation; @@ -282,16 +302,16 @@ async function validateMigrationApplication( /** * Check if migration is already applied by examining actual schema - * + * * This function performs schema introspection to determine if a migration * has already been applied, even if it's not recorded in the migrations * table. This is useful for handling cases where the database schema exists * but the migration tracking got out of sync. - * + * * @param migration - The migration to check * @param sqlQuery - Function to execute SQL queries * @returns Promise resolving to true if schema already exists - * + * * @example * ```typescript * const schemaExists = await isSchemaAlreadyPresent(migration, sqlQuery); @@ -307,33 +327,40 @@ async function isSchemaAlreadyPresent( try { if (migration.name === "001_initial") { // Check if accounts table exists (primary indicator of initial migration) - const result = await sqlQuery(`SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'`) as any; - const hasTable = result?.values?.length > 0 || (Array.isArray(result) && result.length > 0); - console.log(`🔍 [Migration-Schema] Initial migration schema check - accounts table exists: ${hasTable}`); + const result = (await sqlQuery( + `SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'`, + )) as unknown as { values: unknown[][] }; + const hasTable = + result?.values?.length > 0 || + (Array.isArray(result) && result.length > 0); + logger.log( + `🔍 [Migration-Schema] Initial migration schema check - accounts table exists: ${hasTable}`, + ); return hasTable; - } else if (migration.name === "002_add_iViewContent_to_contacts") { // Check if iViewContent column exists in contacts table try { await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`); - console.log(`🔍 [Migration-Schema] iViewContent column already exists`); + logger.log(`🔍 [Migration-Schema] iViewContent column already exists`); return true; } catch (error) { - console.log(`🔍 [Migration-Schema] iViewContent column does not exist`); + logger.log(`🔍 [Migration-Schema] iViewContent column does not exist`); return false; } } - + // Add schema checks for future migrations here // } else if (migration.name === "003_future_migration") { // // Check if future migration schema already exists // } - } catch (error) { - console.log(`🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`, error); + logger.log( + `🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`, + error, + ); return false; } - + return false; } @@ -347,7 +374,7 @@ async function isSchemaAlreadyPresent( * 4. Validates that migrations were applied correctly * 5. Records successful migrations in the tracking table * 6. Performs final validation of the migration state - * + * * The function is designed to be idempotent - it can be run multiple times * safely without re-applying migrations that have already been completed. * @@ -357,22 +384,22 @@ async function isSchemaAlreadyPresent( * @param extractMigrationNames - Function to extract migration names from query results * @returns Promise that resolves when all migrations are complete * @throws {Error} If any migration fails to apply - * + * * @example * ```typescript * // Platform-specific implementation * const sqlExec = async (sql: string, params?: unknown[]) => { * return await db.run(sql, params); * }; - * + * * const sqlQuery = async (sql: string, params?: unknown[]) => { * return await db.query(sql, params); * }; - * + * * const extractNames = (result: DBResult) => { * return new Set(result.values.map(row => row[0])); * }; - * + * * await runMigrations(sqlExec, sqlQuery, extractNames); * ``` */ @@ -382,113 +409,146 @@ export async function runMigrations( extractMigrationNames: (result: T) => Set, ): Promise { try { - console.log("📋 [Migration] Starting migration process..."); - + logger.log("📋 [Migration] Starting migration process..."); + // Step 1: Create migrations table if it doesn't exist // Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration - console.log("🔧 [Migration] Creating migrations table if it doesn't exist..."); + logger.log( + "🔧 [Migration] Creating migrations table if it doesn't exist...", + ); await sqlExec(` CREATE TABLE IF NOT EXISTS migrations ( name TEXT PRIMARY KEY, applied_at TEXT DEFAULT CURRENT_TIMESTAMP ); `); - console.log("✅ [Migration] Migrations table ready"); + logger.log("✅ [Migration] Migrations table ready"); // Step 2: Get list of already applied migrations - console.log("🔍 [Migration] Querying existing migrations..."); + logger.log("🔍 [Migration] Querying existing migrations..."); const appliedMigrationsResult = await sqlQuery( "SELECT name FROM migrations", ); - console.log("📊 [Migration] Raw query result:", appliedMigrationsResult); - + logger.log("📊 [Migration] Raw query result:", appliedMigrationsResult); + const appliedMigrations = extractMigrationNames(appliedMigrationsResult); - console.log("📋 [Migration] Extracted applied migrations:", Array.from(appliedMigrations)); + logger.log( + "📋 [Migration] Extracted applied migrations:", + Array.from(appliedMigrations), + ); // Step 3: Get all registered migrations const migrations = migrationRegistry.getMigrations(); if (migrations.length === 0) { - console.log("⚠️ [Migration] No migrations registered"); + logger.warn("⚠️ [Migration] No migrations registered"); logger.warn("[MigrationService] No migrations registered"); return; } - console.log(`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`); - console.log(`📝 [Migration] Registered migrations: ${migrations.map(m => m.name).join(', ')}`); + logger.log( + `📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`, + ); + logger.log( + `📝 [Migration] Registered migrations: ${migrations.map((m) => m.name).join(", ")}`, + ); let appliedCount = 0; let skippedCount = 0; // Step 4: Process each migration for (const migration of migrations) { - console.log(`\n🔍 [Migration] Processing migration: ${migration.name}`); - + logger.log(`\n🔍 [Migration] Processing migration: ${migration.name}`); + // Check 1: Is it recorded as applied in migrations table? const isRecordedAsApplied = appliedMigrations.has(migration.name); - + // Check 2: Does the schema already exist in the database? const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery); - - console.log(`🔍 [Migration] ${migration.name} - Recorded: ${isRecordedAsApplied}, Schema: ${isSchemaPresent}`); + + logger.log( + `🔍 [Migration] ${migration.name} - Recorded: ${isRecordedAsApplied}, Schema: ${isSchemaPresent}`, + ); // Skip if already recorded as applied if (isRecordedAsApplied) { - console.log(`⏭️ [Migration] Skipping already applied: ${migration.name}`); + logger.log( + `⏭️ [Migration] Skipping already applied: ${migration.name}`, + ); skippedCount++; continue; } // Handle case where schema exists but isn't recorded if (isSchemaPresent) { - console.log(`🔄 [Migration] Schema exists but not recorded. Marking ${migration.name} as applied...`); + logger.log( + `🔄 [Migration] Schema exists but not recorded. Marking ${migration.name} as applied...`, + ); try { - const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ - migration.name, - ]); - console.log(`✅ [Migration] Migration record inserted:`, insertResult); - console.log(`✅ [Migration] Marked existing schema as applied: ${migration.name}`); + const insertResult = await sqlExec( + "INSERT INTO migrations (name) VALUES (?)", + [migration.name], + ); + logger.log(`✅ [Migration] Migration record inserted:`, insertResult); + logger.log( + `✅ [Migration] Marked existing schema as applied: ${migration.name}`, + ); skippedCount++; continue; } catch (insertError) { - console.warn(`⚠️ [Migration] Could not record existing schema ${migration.name}:`, insertError); + logger.warn( + `⚠️ [Migration] Could not record existing schema ${migration.name}:`, + insertError, + ); // Continue with normal migration process as fallback } } // Apply the migration - console.log(`🔄 [Migration] Applying migration: ${migration.name}`); + logger.log(`🔄 [Migration] Applying migration: ${migration.name}`); try { // Execute the migration SQL - console.log(`🔧 [Migration] Executing SQL for ${migration.name}...`); + logger.log(`🔧 [Migration] Executing SQL for ${migration.name}...`); await sqlExec(migration.sql); - console.log(`✅ [Migration] SQL executed successfully for ${migration.name}`); + logger.log( + `✅ [Migration] SQL executed successfully for ${migration.name}`, + ); // Validate the migration was applied correctly - const validation = await validateMigrationApplication(migration, sqlQuery); + const validation = await validateMigrationApplication( + migration, + sqlQuery, + ); if (!validation.isValid) { - console.warn(`⚠️ [Migration] Validation failed for ${migration.name}:`, validation.errors); + logger.warn( + `⚠️ [Migration] Validation failed for ${migration.name}:`, + validation.errors, + ); } else { - console.log(`✅ [Migration] Schema validation passed for ${migration.name}`); + logger.log( + `✅ [Migration] Schema validation passed for ${migration.name}`, + ); } // Record that the migration was applied - console.log(`📝 [Migration] Recording migration ${migration.name} as applied...`); - const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ - migration.name, - ]); - console.log(`✅ [Migration] Migration record inserted:`, insertResult); + logger.log( + `📝 [Migration] Recording migration ${migration.name} as applied...`, + ); + const insertResult = await sqlExec( + "INSERT INTO migrations (name) VALUES (?)", + [migration.name], + ); + logger.log(`✅ [Migration] Migration record inserted:`, insertResult); - console.log(`🎉 [Migration] Successfully applied: ${migration.name}`); + logger.log(`🎉 [Migration] Successfully applied: ${migration.name}`); logger.info( `[MigrationService] Successfully applied migration: ${migration.name}`, ); appliedCount++; - } catch (error) { - console.error(`❌ [Migration] Error applying ${migration.name}:`, error); - + logger.error(`❌ [Migration] Error applying ${migration.name}:`, error); + // Handle specific cases where the migration might be partially applied const errorMessage = String(error).toLowerCase(); @@ -497,33 +557,53 @@ export async function runMigrations( errorMessage.includes("duplicate column") || errorMessage.includes("column already exists") || errorMessage.includes("already exists") || - errorMessage.includes("table") && errorMessage.includes("already exists") + (errorMessage.includes("table") && + errorMessage.includes("already exists")) ) { - console.log(`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Validating and marking as complete.`); - + logger.log( + `⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Validating and marking as complete.`, + ); + // Validate the existing schema - const validation = await validateMigrationApplication(migration, sqlQuery); + const validation = await validateMigrationApplication( + migration, + sqlQuery, + ); if (validation.isValid) { - console.log(`✅ [Migration] Schema validation passed for ${migration.name}`); + logger.log( + `✅ [Migration] Schema validation passed for ${migration.name}`, + ); } else { - console.warn(`⚠️ [Migration] Schema validation failed for ${migration.name}:`, validation.errors); + logger.warn( + `⚠️ [Migration] Schema validation failed for ${migration.name}:`, + validation.errors, + ); } // Mark the migration as applied since the schema change already exists try { - console.log(`📝 [Migration] Attempting to record ${migration.name} as applied despite error...`); - const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ - migration.name, - ]); - console.log(`✅ [Migration] Migration record inserted after error:`, insertResult); - console.log(`✅ [Migration] Marked as applied: ${migration.name}`); + logger.log( + `📝 [Migration] Attempting to record ${migration.name} as applied despite error...`, + ); + const insertResult = await sqlExec( + "INSERT INTO migrations (name) VALUES (?)", + [migration.name], + ); + logger.log( + `✅ [Migration] Migration record inserted after error:`, + insertResult, + ); + logger.log(`✅ [Migration] Marked as applied: ${migration.name}`); logger.info( `[MigrationService] Successfully marked migration as applied: ${migration.name}`, ); appliedCount++; } catch (insertError) { // If we can't insert the migration record, log it but don't fail - console.warn(`⚠️ [Migration] Could not record ${migration.name} as applied:`, insertError); + logger.warn( + `⚠️ [Migration] Could not record ${migration.name} as applied:`, + insertError, + ); logger.warn( `[MigrationService] Could not record migration ${migration.name} as applied:`, insertError, @@ -531,40 +611,55 @@ export async function runMigrations( } } else { // For other types of errors, still fail the migration - console.error(`❌ [Migration] Failed to apply ${migration.name}:`, error); - logger.error( - `[MigrationService] Failed to apply migration ${migration.name}:`, - error, - ); - throw new Error(`Migration ${migration.name} failed: ${error}`); + logger.error( + `❌ [Migration] Failed to apply ${migration.name}:`, + error, + ); + logger.error( + `[MigrationService] Failed to apply migration ${migration.name}:`, + error, + ); + throw new Error(`Migration ${migration.name} failed: ${error}`); + } } } - } // Step 5: Final validation - verify all migrations are properly recorded - console.log("\n🔍 [Migration] Final validation - checking migrations table..."); + logger.log( + "\n🔍 [Migration] Final validation - checking migrations table...", + ); const finalMigrationsResult = await sqlQuery("SELECT name FROM migrations"); const finalAppliedMigrations = extractMigrationNames(finalMigrationsResult); - console.log("📋 [Migration] Final applied migrations:", Array.from(finalAppliedMigrations)); + logger.log( + "📋 [Migration] Final applied migrations:", + Array.from(finalAppliedMigrations), + ); // Check that all expected migrations are recorded - const expectedMigrations = new Set(migrations.map(m => m.name)); - const missingMigrations = [...expectedMigrations].filter(name => !finalAppliedMigrations.has(name)); - + const expectedMigrations = new Set(migrations.map((m) => m.name)); + const missingMigrations = [...expectedMigrations].filter( + (name) => !finalAppliedMigrations.has(name), + ); + if (missingMigrations.length > 0) { - console.warn(`⚠️ [Migration] Missing migration records: ${missingMigrations.join(', ')}`); - logger.warn(`[MigrationService] Missing migration records: ${missingMigrations.join(', ')}`); + logger.warn( + `⚠️ [Migration] Missing migration records: ${missingMigrations.join(", ")}`, + ); + logger.warn( + `[MigrationService] Missing migration records: ${missingMigrations.join(", ")}`, + ); } - console.log(`\n🎉 [Migration] Migration process complete!`); - console.log(`📊 [Migration] Summary: Applied: ${appliedCount}, Skipped: ${skippedCount}, Total: ${migrations.length}`); - logger.info(`[MigrationService] Migration process complete. Applied: ${appliedCount}, Skipped: ${skippedCount}`); - + logger.log(`\n🎉 [Migration] Migration process complete!`); + logger.log( + `📊 [Migration] Summary: Applied: ${appliedCount}, Skipped: ${skippedCount}, Total: ${migrations.length}`, + ); + logger.info( + `[MigrationService] Migration process complete. Applied: ${appliedCount}, Skipped: ${skippedCount}`, + ); } catch (error) { - console.error("\n💥 [Migration] Migration process failed:", error); + logger.error("\n💥 [Migration] Migration process failed:", error); logger.error("[MigrationService] Migration process failed:", error); throw error; } } - - diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index d2d7a471..a77a094e 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -239,42 +239,42 @@ export class CapacitorPlatformService implements PlatformService { /** * Execute database migrations for the Capacitor platform - * + * * This method orchestrates the database migration process specifically for * Capacitor-based platforms (mobile and Electron). It provides the platform-specific * SQL execution functions to the migration service and handles Capacitor SQLite * plugin integration. - * + * * ## Migration Process: - * + * * 1. **SQL Execution Setup**: Creates platform-specific SQL execution functions * that properly handle the Capacitor SQLite plugin's API - * + * * 2. **Parameter Handling**: Ensures proper parameter binding for prepared statements * using the correct Capacitor SQLite methods (run vs execute) - * + * * 3. **Result Parsing**: Provides extraction functions that understand the * Capacitor SQLite result format - * + * * 4. **Migration Execution**: Delegates to the migration service for the actual * migration logic and tracking - * + * * 5. **Integrity Verification**: Runs post-migration integrity checks to ensure * the database is in the expected state - * + * * ## Error Handling: - * + * * The method includes comprehensive error handling for: * - Database connection issues * - SQL execution failures * - Migration tracking problems * - Schema validation errors - * + * * Even if migrations fail, the integrity check still runs to assess the * current database state and provide debugging information. - * + * * ## Logging: - * + * * Detailed logging is provided throughout the process using emoji-tagged * console messages that appear in the Electron DevTools console. This * includes: @@ -282,10 +282,10 @@ export class CapacitorPlatformService implements PlatformService { * - Parameter values for debugging * - Migration success/failure status * - Database integrity check results - * + * * @throws {Error} If database is not initialized or migrations fail critically * @private Internal method called during database initialization - * + * * @example * ```typescript * // Called automatically during platform service initialization @@ -299,98 +299,110 @@ export class CapacitorPlatformService implements PlatformService { /** * SQL execution function for Capacitor SQLite plugin - * + * * This function handles the execution of SQL statements (INSERT, UPDATE, CREATE, etc.) * through the Capacitor SQLite plugin. It automatically chooses the appropriate * method based on whether parameters are provided. - * + * * @param sql - SQL statement to execute * @param params - Optional parameters for prepared statements * @returns Promise resolving to execution results */ - const sqlExec = async (sql: string, params?: unknown[]): Promise => { - console.log(`🔧 [CapacitorMigration] Executing SQL:`, sql); - console.log(`📋 [CapacitorMigration] With params:`, params); - + const sqlExec = async ( + sql: string, + params?: unknown[], + ): Promise => { + logger.log(`🔧 [CapacitorMigration] Executing SQL:`, sql); + logger.log(`📋 [CapacitorMigration] With params:`, params); + if (params && params.length > 0) { // Use run method for parameterized queries (prepared statements) // This is essential for proper parameter binding and SQL injection prevention const result = await this.db!.run(sql, params); - console.log(`✅ [CapacitorMigration] Run result:`, result); + logger.log(`✅ [CapacitorMigration] Run result:`, result); return result; } else { // Use execute method for non-parameterized queries // This is more efficient for simple DDL statements const result = await this.db!.execute(sql); - console.log(`✅ [CapacitorMigration] Execute result:`, result); + logger.log(`✅ [CapacitorMigration] Execute result:`, result); return result; } }; /** * SQL query function for Capacitor SQLite plugin - * + * * This function handles the execution of SQL queries (SELECT statements) * through the Capacitor SQLite plugin. It returns the raw result data * that can be processed by the migration service. - * + * * @param sql - SQL query to execute * @param params - Optional parameters for prepared statements * @returns Promise resolving to query results */ - const sqlQuery = async (sql: string, params?: unknown[]): Promise => { - console.log(`🔍 [CapacitorMigration] Querying SQL:`, sql); - console.log(`📋 [CapacitorMigration] With params:`, params); - + const sqlQuery = async ( + sql: string, + params?: unknown[], + ): Promise => { + logger.log(`🔍 [CapacitorMigration] Querying SQL:`, sql); + logger.log(`📋 [CapacitorMigration] With params:`, params); + const result = await this.db!.query(sql, params); - console.log(`📊 [CapacitorMigration] Query result:`, result); + logger.log(`📊 [CapacitorMigration] Query result:`, result); return result; }; /** * Extract migration names from Capacitor SQLite query results - * + * * This function parses the result format returned by the Capacitor SQLite * plugin and extracts migration names. It handles the specific data structure * used by the plugin, which can vary between different result formats. - * + * * ## Result Format Handling: - * + * * The Capacitor SQLite plugin can return results in different formats: * - Object format: `{ name: "migration_name" }` * - Array format: `["migration_name", "timestamp"]` - * + * * This function handles both formats to ensure robust migration name extraction. - * + * * @param result - Query result from Capacitor SQLite plugin * @returns Set of migration names found in the result */ const extractMigrationNames = (result: DBSQLiteValues): Set => { - console.log(`🔍 [CapacitorMigration] Extracting migration names from:`, result); - + logger.log( + `🔍 [CapacitorMigration] Extracting migration names from:`, + result, + ); + // Handle the Capacitor SQLite result format - const names = result.values?.map((row: any) => { - // The row could be an object with 'name' property or an array where name is first element - if (typeof row === 'object' && row.name !== undefined) { - return row.name; - } else if (Array.isArray(row) && row.length > 0) { - return row[0]; - } - return null; - }).filter(name => name !== null) || []; - - console.log(`📋 [CapacitorMigration] Extracted names:`, names); + const names = + result.values + ?.map((row: unknown) => { + // The row could be an object with 'name' property or an array where name is first element + if (typeof row === "object" && row !== null && "name" in row) { + return (row as { name: string }).name; + } else if (Array.isArray(row) && row.length > 0) { + return row[0]; + } + return null; + }) + .filter((name) => name !== null) || []; + + logger.log(`📋 [CapacitorMigration] Extracted names:`, names); return new Set(names); }; try { // Execute the migration process - await runMigrations(sqlExec, sqlQuery, extractMigrationNames); - + await runMigrations(sqlExec, sqlQuery, extractMigrationNames); + // After migrations, run integrity check to verify database state await this.verifyDatabaseIntegrity(); } catch (error) { - console.error(`❌ [CapacitorMigration] Migration failed:`, error); + logger.error(`❌ [CapacitorMigration] Migration failed:`, error); // Still try to verify what we have for debugging purposes await this.verifyDatabaseIntegrity(); throw error; @@ -399,50 +411,50 @@ export class CapacitorPlatformService implements PlatformService { /** * Verify database integrity and migration status - * + * * This method performs comprehensive validation of the database structure * and migration state. It's designed to help identify issues with the * migration process and provide detailed debugging information. - * + * * ## Validation Steps: - * + * * 1. **Migration Records**: Checks which migrations are recorded as applied * 2. **Table Existence**: Verifies all expected core tables exist * 3. **Schema Validation**: Checks table schemas including column presence * 4. **Data Integrity**: Validates basic data counts and structure - * + * * ## Core Tables Validated: - * + * * - `accounts`: User identity and cryptographic keys * - `secret`: Application secrets and encryption keys * - `settings`: Configuration and user preferences * - `contacts`: Contact network and trust relationships * - `logs`: Application event logging * - `temp`: Temporary data storage - * + * * ## Schema Checks: - * + * * For critical tables like `contacts`, the method validates: * - Table structure using `PRAGMA table_info` * - Presence of important columns (e.g., `iViewContent`) * - Column data types and constraints - * + * * ## Error Handling: - * + * * This method is designed to never throw errors - it captures and logs * all validation issues for debugging purposes. This ensures that even * if integrity checks fail, they don't prevent the application from starting. - * + * * ## Logging Output: - * + * * The method produces detailed console output with emoji tags: * - `✅` for successful validations * - `❌` for validation failures * - `📊` for data summaries * - `🔍` for investigation steps - * + * * @private Internal method called after migrations - * + * * @example * ```typescript * // Called automatically after migration completion @@ -451,70 +463,109 @@ export class CapacitorPlatformService implements PlatformService { */ private async verifyDatabaseIntegrity(): Promise { if (!this.db) { - console.error(`❌ [DB-Integrity] Database not initialized`); + logger.error(`❌ [DB-Integrity] Database not initialized`); return; } - console.log(`🔍 [DB-Integrity] Starting database integrity check...`); + logger.log(`🔍 [DB-Integrity] Starting database integrity check...`); try { // Step 1: Check migrations table and applied migrations - const migrationsResult = await this.db.query("SELECT name, applied_at FROM migrations ORDER BY applied_at"); - console.log(`📊 [DB-Integrity] Applied migrations:`, migrationsResult); + const migrationsResult = await this.db.query( + "SELECT name, applied_at FROM migrations ORDER BY applied_at", + ); + logger.log(`📊 [DB-Integrity] Applied migrations:`, migrationsResult); // Step 2: Verify core tables exist - const coreTableNames = ['accounts', 'secret', 'settings', 'contacts', 'logs', 'temp']; + const coreTableNames = [ + "accounts", + "secret", + "settings", + "contacts", + "logs", + "temp", + ]; const existingTables: string[] = []; - + for (const tableName of coreTableNames) { try { - const tableCheck = await this.db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`); + const tableCheck = await this.db.query( + `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`, + ); if (tableCheck.values && tableCheck.values.length > 0) { existingTables.push(tableName); - console.log(`✅ [DB-Integrity] Table ${tableName} exists`); + logger.log(`✅ [DB-Integrity] Table ${tableName} exists`); } else { - console.error(`❌ [DB-Integrity] Table ${tableName} missing`); + logger.error(`❌ [DB-Integrity] Table ${tableName} missing`); } } catch (error) { - console.error(`❌ [DB-Integrity] Error checking table ${tableName}:`, error); + logger.error( + `❌ [DB-Integrity] Error checking table ${tableName}:`, + error, + ); } } // Step 3: Check contacts table schema (including iViewContent column) - if (existingTables.includes('contacts')) { + if (existingTables.includes("contacts")) { try { - const contactsSchema = await this.db.query("PRAGMA table_info(contacts)"); - console.log(`📊 [DB-Integrity] Contacts table schema:`, contactsSchema); - + const contactsSchema = await this.db.query( + "PRAGMA table_info(contacts)", + ); + logger.log( + `📊 [DB-Integrity] Contacts table schema:`, + contactsSchema, + ); + // Check for iViewContent column specifically - const hasIViewContent = contactsSchema.values?.some((col: any) => - (col.name === 'iViewContent') || (Array.isArray(col) && col[1] === 'iViewContent') + const hasIViewContent = contactsSchema.values?.some( + (col: unknown) => + (typeof col === "object" && + col !== null && + "name" in col && + (col as { name: string }).name === "iViewContent") || + (Array.isArray(col) && col[1] === "iViewContent"), ); - + if (hasIViewContent) { - console.log(`✅ [DB-Integrity] iViewContent column exists in contacts table`); + logger.log( + `✅ [DB-Integrity] iViewContent column exists in contacts table`, + ); } else { - console.error(`❌ [DB-Integrity] iViewContent column missing from contacts table`); + logger.error( + `❌ [DB-Integrity] iViewContent column missing from contacts table`, + ); } } catch (error) { - console.error(`❌ [DB-Integrity] Error checking contacts schema:`, error); + logger.error( + `❌ [DB-Integrity] Error checking contacts schema:`, + error, + ); } } // Step 4: Check for basic data integrity try { - const accountCount = await this.db.query("SELECT COUNT(*) as count FROM accounts"); - const settingsCount = await this.db.query("SELECT COUNT(*) as count FROM settings"); - const contactsCount = await this.db.query("SELECT COUNT(*) as count FROM contacts"); - - console.log(`📊 [DB-Integrity] Data counts - Accounts: ${JSON.stringify(accountCount)}, Settings: ${JSON.stringify(settingsCount)}, Contacts: ${JSON.stringify(contactsCount)}`); + const accountCount = await this.db.query( + "SELECT COUNT(*) as count FROM accounts", + ); + const settingsCount = await this.db.query( + "SELECT COUNT(*) as count FROM settings", + ); + const contactsCount = await this.db.query( + "SELECT COUNT(*) as count FROM contacts", + ); + + logger.log( + `📊 [DB-Integrity] Data counts - Accounts: ${JSON.stringify(accountCount)}, Settings: ${JSON.stringify(settingsCount)}, Contacts: ${JSON.stringify(contactsCount)}`, + ); } catch (error) { - console.error(`❌ [DB-Integrity] Error checking data counts:`, error); + logger.error(`❌ [DB-Integrity] Error checking data counts:`, error); } - console.log(`✅ [DB-Integrity] Database integrity check completed`); + logger.log(`✅ [DB-Integrity] Database integrity check completed`); } catch (error) { - console.error(`❌ [DB-Integrity] Database integrity check failed:`, error); + logger.error(`❌ [DB-Integrity] Database integrity check failed:`, error); } }