From 25c1d6ef4ebd8290ea0560746016abfa5c67748d Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 18 Jun 2025 10:54:32 +0000 Subject: [PATCH 01/29] feat: Add comprehensive database migration service for Dexie to SQLite - Add migrationService.ts with functions to compare and transfer data between Dexie and SQLite - Implement data comparison with detailed difference analysis (added/modified/missing) - Add contact migration with overwrite options and error handling - Add settings migration focusing on key user fields (firstName, isRegistered, profileImageUrl, showShortcutBvc, searchBoxes) - Include YAML export functionality for data inspection - Add comprehensive JSDoc documentation with examples and usage instructions - Support both INSERT and UPDATE operations with parameterized SQL generation - Include detailed logging and error reporting for migration operations This service enables safe migration of user data from the legacy Dexie (IndexedDB) database to the new SQLite implementation, with full comparison capabilities and rollback safety through detailed reporting. --- doc/dexie-to-sqlite-mapping.md | 41 +- src/db/tables/README.md | 2 +- src/services/migrationService.ts | 1019 ++++++++++++++++++++++++++++-- 3 files changed, 996 insertions(+), 66 deletions(-) diff --git a/doc/dexie-to-sqlite-mapping.md b/doc/dexie-to-sqlite-mapping.md index 893b4670..e598cdcf 100644 --- a/doc/dexie-to-sqlite-mapping.md +++ b/doc/dexie-to-sqlite-mapping.md @@ -3,6 +3,7 @@ ## Schema Mapping ### Current Dexie Schema + ```typescript // Current Dexie schema const db = new Dexie('TimeSafariDB'); @@ -15,6 +16,7 @@ db.version(1).stores({ ``` ### New SQLite Schema + ```sql -- New SQLite schema CREATE TABLE accounts ( @@ -50,6 +52,7 @@ CREATE INDEX idx_settings_updated_at ON settings(updated_at); ### 1. Account Operations #### Get Account by DID + ```typescript // Dexie const account = await db.accounts.get(did); @@ -62,6 +65,7 @@ const account = result[0]?.values[0]; ``` #### Get All Accounts + ```typescript // Dexie const accounts = await db.accounts.toArray(); @@ -74,6 +78,7 @@ const accounts = result[0]?.values || []; ``` #### Add Account + ```typescript // Dexie await db.accounts.add({ @@ -91,6 +96,7 @@ await db.run(` ``` #### Update Account + ```typescript // Dexie await db.accounts.update(did, { @@ -100,7 +106,7 @@ await db.accounts.update(did, { // absurd-sql await db.run(` - UPDATE accounts + UPDATE accounts SET public_key_hex = ?, updated_at = ? WHERE did = ? `, [publicKeyHex, Date.now(), did]); @@ -109,6 +115,7 @@ await db.run(` ### 2. Settings Operations #### Get Setting + ```typescript // Dexie const setting = await db.settings.get(key); @@ -121,6 +128,7 @@ const setting = result[0]?.values[0]; ``` #### Set Setting + ```typescript // Dexie await db.settings.put({ @@ -142,6 +150,7 @@ await db.run(` ### 3. Contact Operations #### Get Contacts by Account + ```typescript // Dexie const contacts = await db.contacts @@ -151,7 +160,7 @@ const contacts = await db.contacts // absurd-sql const result = await db.exec(` - SELECT * FROM contacts + SELECT * FROM contacts WHERE did = ? ORDER BY created_at DESC `, [accountDid]); @@ -159,6 +168,7 @@ const contacts = result[0]?.values || []; ``` #### Add Contact + ```typescript // Dexie await db.contacts.add({ @@ -179,6 +189,7 @@ await db.run(` ## Transaction Mapping ### Batch Operations + ```typescript // Dexie await db.transaction('rw', [db.accounts, db.contacts], async () => { @@ -210,10 +221,11 @@ try { ## Migration Helper Functions ### 1. Data Export (Dexie to JSON) + ```typescript async function exportDexieData(): Promise { const db = new Dexie('TimeSafariDB'); - + return { accounts: await db.accounts.toArray(), settings: await db.settings.toArray(), @@ -228,6 +240,7 @@ async function exportDexieData(): Promise { ``` ### 2. Data Import (JSON to absurd-sql) + ```typescript async function importToAbsurdSql(data: MigrationData): Promise { await db.exec('BEGIN TRANSACTION;'); @@ -239,7 +252,7 @@ async function importToAbsurdSql(data: MigrationData): Promise { VALUES (?, ?, ?, ?) `, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]); } - + // Import settings for (const setting of data.settings) { await db.run(` @@ -247,7 +260,7 @@ async function importToAbsurdSql(data: MigrationData): Promise { VALUES (?, ?, ?) `, [setting.key, setting.value, setting.updatedAt]); } - + // Import contacts for (const contact of data.contacts) { await db.run(` @@ -264,6 +277,7 @@ async function importToAbsurdSql(data: MigrationData): Promise { ``` ### 3. Verification + ```typescript async function verifyMigration(dexieData: MigrationData): Promise { // Verify account count @@ -272,21 +286,21 @@ async function verifyMigration(dexieData: MigrationData): Promise { if (accountCount !== dexieData.accounts.length) { return false; } - + // Verify settings count const settingsResult = await db.exec('SELECT COUNT(*) as count FROM settings'); const settingsCount = settingsResult[0].values[0][0]; if (settingsCount !== dexieData.settings.length) { return false; } - + // Verify contacts count const contactsResult = await db.exec('SELECT COUNT(*) as count FROM contacts'); const contactsCount = contactsResult[0].values[0][0]; if (contactsCount !== dexieData.contacts.length) { return false; } - + // Verify data integrity for (const account of dexieData.accounts) { const result = await db.exec( @@ -294,12 +308,12 @@ async function verifyMigration(dexieData: MigrationData): Promise { [account.did] ); const migratedAccount = result[0]?.values[0]; - if (!migratedAccount || + if (!migratedAccount || migratedAccount[1] !== account.publicKeyHex) { // public_key_hex is second column return false; } } - + return true; } ``` @@ -307,18 +321,21 @@ async function verifyMigration(dexieData: MigrationData): Promise { ## Performance Considerations ### 1. Indexing + - Dexie automatically creates indexes based on the schema - absurd-sql requires explicit index creation - Added indexes for frequently queried fields - Use `PRAGMA journal_mode=MEMORY;` for better performance ### 2. Batch Operations + - Dexie has built-in bulk operations - absurd-sql uses transactions for batch operations - Consider chunking large datasets - Use prepared statements for repeated queries ### 3. Query Optimization + - Dexie uses IndexedDB's native indexing - absurd-sql requires explicit query optimization - Use prepared statements for repeated queries @@ -327,6 +344,7 @@ async function verifyMigration(dexieData: MigrationData): Promise { ## Error Handling ### 1. Common Errors + ```typescript // Dexie errors try { @@ -351,6 +369,7 @@ try { ``` ### 2. Transaction Recovery + ```typescript // Dexie transaction try { @@ -396,4 +415,4 @@ try { - Remove Dexie database - Clear IndexedDB storage - Update application code - - Remove old dependencies \ No newline at end of file + - Remove old dependencies diff --git a/src/db/tables/README.md b/src/db/tables/README.md index ae2f2688..617dae94 100644 --- a/src/db/tables/README.md +++ b/src/db/tables/README.md @@ -1 +1 @@ -Check the contact & settings export to see whether you want your new table to be included in it. +# Check the contact & settings export to see whether you want your new table to be included in it diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 81d9de74..c8dc4e84 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -1,60 +1,971 @@ -interface Migration { - name: string; - sql: string; -} - -export class MigrationService { - private static instance: MigrationService; - private migrations: Migration[] = []; - - private constructor() {} - - static getInstance(): MigrationService { - if (!MigrationService.instance) { - MigrationService.instance = new MigrationService(); - } - return MigrationService.instance; - } - - registerMigration(migration: Migration) { - this.migrations.push(migration); - } - - /** - * @param sqlExec - A function that executes a SQL statement and returns some update result - * @param sqlQuery - A function that executes a SQL query and returns the result in some format - * @param extractMigrationNames - A function that extracts the names (string array) from a "select name from migrations" query - */ - async runMigrations( - // note that this does not take parameters because the Capacitor SQLite 'execute' is different - sqlExec: (sql: string) => Promise, - sqlQuery: (sql: string) => Promise, - extractMigrationNames: (result: T) => Set, - ): Promise { - // Create migrations table if it doesn't exist - await sqlExec(` - CREATE TABLE IF NOT EXISTS migrations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - `); - - // Get list of executed migrations - const result1: T = await sqlQuery("SELECT name FROM migrations;"); - const executedMigrations = extractMigrationNames(result1); - - // Run pending migrations in order - for (const migration of this.migrations) { - if (!executedMigrations.has(migration.name)) { - await sqlExec(migration.sql); - - await sqlExec( - `INSERT INTO migrations (name) VALUES ('${migration.name}')`, +/** + * Migration Service for transferring data from Dexie (IndexedDB) to SQLite + * + * This service provides functions to: + * 1. Compare data between Dexie and SQLite databases + * 2. Transfer contacts and settings from Dexie to SQLite + * 3. Generate YAML-formatted data comparisons + * + * The service is designed to work with the TimeSafari app's dual database architecture, + * where data can exist in both Dexie (IndexedDB) and SQLite databases. This allows + * for safe migration of data between the two storage systems. + * + * Usage: + * 1. Enable Dexie temporarily by setting USE_DEXIE_DB = true in constants/app.ts + * 2. Use compareDatabases() to see differences between databases + * 3. Use migrateContacts() and/or migrateSettings() to transfer data + * 4. Disable Dexie again after migration is complete + * + * @author Matthew Raymer + * @version 1.0.0 + * @since 2024 + */ + +import { PlatformServiceFactory } from "./PlatformServiceFactory"; +import { db } from "../db/index"; +import { Contact, ContactMethod } from "../db/tables/contacts"; +import { Settings } from "../db/tables/settings"; +import { logger } from "../utils/logger"; +import { parseJsonField } from "../db/databaseUtil"; +import { USE_DEXIE_DB } from "../constants/app"; + +/** + * Interface for data comparison results between Dexie and SQLite databases + * + * This interface provides a comprehensive view of the differences between + * the two database systems, including counts and detailed lists of + * added, modified, and missing records. + * + * @interface DataComparison + * @property {Contact[]} dexieContacts - All contacts from Dexie database + * @property {Contact[]} sqliteContacts - All contacts from SQLite database + * @property {Settings[]} dexieSettings - All settings from Dexie database + * @property {Settings[]} sqliteSettings - All settings from SQLite database + * @property {Object} differences - Detailed differences between databases + * @property {Object} differences.contacts - Contact-specific differences + * @property {Contact[]} differences.contacts.added - Contacts in Dexie but not SQLite + * @property {Contact[]} differences.contacts.modified - Contacts that differ between databases + * @property {Contact[]} differences.contacts.missing - Contacts in SQLite but not Dexie + * @property {Object} differences.settings - Settings-specific differences + * @property {Settings[]} differences.settings.added - Settings in Dexie but not SQLite + * @property {Settings[]} differences.settings.modified - Settings that differ between databases + * @property {Settings[]} differences.settings.missing - Settings in SQLite but not Dexie + */ +export interface DataComparison { + dexieContacts: Contact[]; + sqliteContacts: Contact[]; + dexieSettings: Settings[]; + sqliteSettings: Settings[]; + differences: { + contacts: { + added: Contact[]; + modified: Contact[]; + missing: Contact[]; + }; + settings: { + added: Settings[]; + modified: Settings[]; + missing: Settings[]; + }; + }; +} + +/** + * Interface for migration operation results + * + * Provides detailed feedback about the success or failure of migration + * operations, including counts of migrated records and any errors or + * warnings that occurred during the process. + * + * @interface MigrationResult + * @property {boolean} success - Whether the migration operation completed successfully + * @property {number} contactsMigrated - Number of contacts successfully migrated + * @property {number} settingsMigrated - Number of settings successfully migrated + * @property {string[]} errors - Array of error messages encountered during migration + * @property {string[]} warnings - Array of warning messages (non-fatal issues) + */ +export interface MigrationResult { + success: boolean; + contactsMigrated: number; + settingsMigrated: number; + errors: string[]; + warnings: string[]; +} + +/** + * Retrieves all contacts from the Dexie (IndexedDB) database + * + * This function connects to the Dexie database and retrieves all contact + * records. It requires that USE_DEXIE_DB is enabled in the app constants. + * + * The function handles database opening and error conditions, providing + * detailed logging for debugging purposes. + * + * @async + * @function getDexieContacts + * @returns {Promise} Array of all contacts from Dexie database + * @throws {Error} If Dexie database is not enabled or if database access fails + * @example + * ```typescript + * try { + * const contacts = await getDexieContacts(); + * console.log(`Retrieved ${contacts.length} contacts from Dexie`); + * } catch (error) { + * console.error('Failed to retrieve Dexie contacts:', error); + * } + * ``` + */ +export async function getDexieContacts(): Promise { + if (!USE_DEXIE_DB) { + throw new Error("Dexie database is not enabled"); + } + + try { + await db.open(); + const contacts = await db.contacts.toArray(); + logger.info(`[MigrationService] Retrieved ${contacts.length} contacts from Dexie`); + return contacts; + } catch (error) { + logger.error("[MigrationService] Error retrieving Dexie contacts:", error); + throw new Error(`Failed to retrieve Dexie contacts: ${error}`); + } +} + +/** + * Retrieves all contacts from the SQLite database + * + * This function uses the platform service to query the SQLite database + * and retrieve all contact records. It handles the conversion of raw + * database results into properly typed Contact objects. + * + * The function also handles JSON parsing for complex fields like + * contactMethods, ensuring proper type conversion. + * + * @async + * @function getSqliteContacts + * @returns {Promise} Array of all contacts from SQLite database + * @throws {Error} If database query fails or data conversion fails + * @example + * ```typescript + * try { + * const contacts = await getSqliteContacts(); + * console.log(`Retrieved ${contacts.length} contacts from SQLite`); + * } catch (error) { + * console.error('Failed to retrieve SQLite contacts:', error); + * } + * ``` + */ +export async function getSqliteContacts(): Promise { + try { + const platformService = PlatformServiceFactory.getInstance(); + const result = await platformService.dbQuery("SELECT * FROM contacts"); + + if (!result?.values?.length) { + return []; + } + + const contacts = result.values.map((row) => { + const contact = parseJsonField(row, {}) as any; + return { + did: contact.did || "", + name: contact.name || "", + contactMethods: parseJsonField(contact.contactMethods, []) as ContactMethod[], + nextPubKeyHashB64: contact.nextPubKeyHashB64 || "", + notes: contact.notes || "", + profileImageUrl: contact.profileImageUrl || "", + publicKeyBase64: contact.publicKeyBase64 || "", + seesMe: contact.seesMe || false, + registered: contact.registered || false, + } as Contact; + }); + + logger.info(`[MigrationService] Retrieved ${contacts.length} contacts from SQLite`); + return contacts; + } catch (error) { + logger.error("[MigrationService] Error retrieving SQLite contacts:", error); + throw new Error(`Failed to retrieve SQLite contacts: ${error}`); + } +} + +/** + * Retrieves all settings from the Dexie (IndexedDB) database + * + * This function connects to the Dexie database and retrieves all settings + * records. It requires that USE_DEXIE_DB is enabled in the app constants. + * + * Settings include both master settings (id=1) and account-specific settings + * that override the master settings for particular user accounts. + * + * @async + * @function getDexieSettings + * @returns {Promise} Array of all settings from Dexie database + * @throws {Error} If Dexie database is not enabled or if database access fails + * @example + * ```typescript + * try { + * const settings = await getDexieSettings(); + * console.log(`Retrieved ${settings.length} settings from Dexie`); + * } catch (error) { + * console.error('Failed to retrieve Dexie settings:', error); + * } + * ``` + */ +export async function getDexieSettings(): Promise { + if (!USE_DEXIE_DB) { + throw new Error("Dexie database is not enabled"); + } + + try { + await db.open(); + const settings = await db.settings.toArray(); + logger.info(`[MigrationService] Retrieved ${settings.length} settings from Dexie`); + return settings; + } catch (error) { + logger.error("[MigrationService] Error retrieving Dexie settings:", error); + throw new Error(`Failed to retrieve Dexie settings: ${error}`); + } +} + +/** + * Retrieves all settings from the SQLite database + * + * This function uses the platform service to query the SQLite database + * and retrieve all settings records. It handles the conversion of raw + * database results into properly typed Settings objects. + * + * The function also handles JSON parsing for complex fields like + * searchBoxes, ensuring proper type conversion. + * + * @async + * @function getSqliteSettings + * @returns {Promise} Array of all settings from SQLite database + * @throws {Error} If database query fails or data conversion fails + * @example + * ```typescript + * try { + * const settings = await getSqliteSettings(); + * console.log(`Retrieved ${settings.length} settings from SQLite`); + * } catch (error) { + * console.error('Failed to retrieve SQLite settings:', error); + * } + * ``` + */ +export async function getSqliteSettings(): Promise { + try { + const platformService = PlatformServiceFactory.getInstance(); + const result = await platformService.dbQuery("SELECT * FROM settings"); + + if (!result?.values?.length) { + return []; + } + + const settings = result.values.map((row) => { + const setting = parseJsonField(row, {}) as any; + return { + id: setting.id, + accountDid: setting.accountDid || "", + activeDid: setting.activeDid || "", + apiServer: setting.apiServer || "", + filterFeedByNearby: setting.filterFeedByNearby || false, + filterFeedByVisible: setting.filterFeedByVisible || false, + finishedOnboarding: setting.finishedOnboarding || false, + firstName: setting.firstName || "", + hideRegisterPromptOnNewContact: setting.hideRegisterPromptOnNewContact || false, + isRegistered: setting.isRegistered || false, + lastName: setting.lastName || "", + lastAckedOfferToUserJwtId: setting.lastAckedOfferToUserJwtId || "", + lastAckedOfferToUserProjectsJwtId: setting.lastAckedOfferToUserProjectsJwtId || "", + lastNotifiedClaimId: setting.lastNotifiedClaimId || "", + lastViewedClaimId: setting.lastViewedClaimId || "", + notifyingNewActivityTime: setting.notifyingNewActivityTime || "", + notifyingReminderMessage: setting.notifyingReminderMessage || "", + notifyingReminderTime: setting.notifyingReminderTime || "", + partnerApiServer: setting.partnerApiServer || "", + passkeyExpirationMinutes: setting.passkeyExpirationMinutes, + profileImageUrl: setting.profileImageUrl || "", + searchBoxes: parseJsonField(setting.searchBoxes, []), + showContactGivesInline: setting.showContactGivesInline || false, + showGeneralAdvanced: setting.showGeneralAdvanced || false, + showShortcutBvc: setting.showShortcutBvc || false, + vapid: setting.vapid || "", + warnIfProdServer: setting.warnIfProdServer || false, + warnIfTestServer: setting.warnIfTestServer || false, + webPushServer: setting.webPushServer || "", + } as Settings; + }); + + logger.info(`[MigrationService] Retrieved ${settings.length} settings from SQLite`); + return settings; + } catch (error) { + logger.error("[MigrationService] Error retrieving SQLite settings:", error); + throw new Error(`Failed to retrieve SQLite settings: ${error}`); + } +} + +/** + * Compares data between Dexie and SQLite databases + * + * This is the main comparison function that retrieves data from both + * databases and identifies differences. It provides a comprehensive + * view of what data exists in each database and what needs to be + * migrated. + * + * The function performs parallel data retrieval for efficiency and + * then compares the results to identify added, modified, and missing + * records in each table. + * + * @async + * @function compareDatabases + * @returns {Promise} Comprehensive comparison results + * @throws {Error} If any database access fails + * @example + * ```typescript + * try { + * const comparison = await compareDatabases(); + * console.log(`Dexie contacts: ${comparison.dexieContacts.length}`); + * console.log(`SQLite contacts: ${comparison.sqliteContacts.length}`); + * console.log(`Added contacts: ${comparison.differences.contacts.added.length}`); + * } catch (error) { + * console.error('Database comparison failed:', error); + * } + * ``` + */ +export async function compareDatabases(): Promise { + logger.info("[MigrationService] Starting database comparison"); + + const [dexieContacts, sqliteContacts, dexieSettings, sqliteSettings] = await Promise.all([ + getDexieContacts(), + getSqliteContacts(), + getDexieSettings(), + getSqliteSettings(), + ]); + + // Compare contacts + const contactDifferences = compareContacts(dexieContacts, sqliteContacts); + + // Compare settings + const settingsDifferences = compareSettings(dexieSettings, sqliteSettings); + + const comparison: DataComparison = { + dexieContacts, + sqliteContacts, + dexieSettings, + sqliteSettings, + differences: { + contacts: contactDifferences, + settings: settingsDifferences, + }, + }; + + logger.info("[MigrationService] Database comparison completed", { + dexieContacts: dexieContacts.length, + sqliteContacts: sqliteContacts.length, + dexieSettings: dexieSettings.length, + sqliteSettings: sqliteSettings.length, + contactDifferences: contactDifferences, + settingsDifferences: settingsDifferences, + }); + + return comparison; +} + +/** + * Compares contacts between Dexie and SQLite databases + * + * This helper function analyzes two arrays of contacts and identifies + * which contacts are added (in Dexie but not SQLite), modified + * (different between databases), or missing (in SQLite but not Dexie). + * + * The comparison is based on the contact's DID (Decentralized Identifier) + * as the primary key, with detailed field-by-field comparison for + * modified contacts. + * + * @function compareContacts + * @param {Contact[]} dexieContacts - Contacts from Dexie database + * @param {Contact[]} sqliteContacts - Contacts from SQLite database + * @returns {Object} Object containing added, modified, and missing contacts + * @returns {Contact[]} returns.added - Contacts in Dexie but not SQLite + * @returns {Contact[]} returns.modified - Contacts that differ between databases + * @returns {Contact[]} returns.missing - Contacts in SQLite but not Dexie + * @example + * ```typescript + * const differences = compareContacts(dexieContacts, sqliteContacts); + * console.log(`Added: ${differences.added.length}`); + * console.log(`Modified: ${differences.modified.length}`); + * console.log(`Missing: ${differences.missing.length}`); + * ``` + */ +function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) { + const added: Contact[] = []; + const modified: Contact[] = []; + const missing: Contact[] = []; + + // Find contacts that exist in Dexie but not in SQLite + for (const dexieContact of dexieContacts) { + const sqliteContact = sqliteContacts.find(c => c.did === dexieContact.did); + if (!sqliteContact) { + added.push(dexieContact); + } else if (!contactsEqual(dexieContact, sqliteContact)) { + modified.push(dexieContact); + } + } + + // Find contacts that exist in SQLite but not in Dexie + for (const sqliteContact of sqliteContacts) { + const dexieContact = dexieContacts.find(c => c.did === sqliteContact.did); + if (!dexieContact) { + missing.push(sqliteContact); + } + } + + return { added, modified, missing }; +} + +/** + * Compares settings between Dexie and SQLite databases + * + * This helper function analyzes two arrays of settings and identifies + * which settings are added (in Dexie but not SQLite), modified + * (different between databases), or missing (in SQLite but not Dexie). + * + * The comparison is based on the setting's ID as the primary key, + * with detailed field-by-field comparison for modified settings. + * + * @function compareSettings + * @param {Settings[]} dexieSettings - Settings from Dexie database + * @param {Settings[]} sqliteSettings - Settings from SQLite database + * @returns {Object} Object containing added, modified, and missing settings + * @returns {Settings[]} returns.added - Settings in Dexie but not SQLite + * @returns {Settings[]} returns.modified - Settings that differ between databases + * @returns {Settings[]} returns.missing - Settings in SQLite but not Dexie + * @example + * ```typescript + * const differences = compareSettings(dexieSettings, sqliteSettings); + * console.log(`Added: ${differences.added.length}`); + * console.log(`Modified: ${differences.modified.length}`); + * console.log(`Missing: ${differences.missing.length}`); + * ``` + */ +function compareSettings(dexieSettings: Settings[], sqliteSettings: Settings[]) { + const added: Settings[] = []; + const modified: Settings[] = []; + const missing: Settings[] = []; + + // Find settings that exist in Dexie but not in SQLite + for (const dexieSetting of dexieSettings) { + const sqliteSetting = sqliteSettings.find(s => s.id === dexieSetting.id); + if (!sqliteSetting) { + added.push(dexieSetting); + } else if (!settingsEqual(dexieSetting, sqliteSetting)) { + modified.push(dexieSetting); + } + } + + // Find settings that exist in SQLite but not in Dexie + for (const sqliteSetting of sqliteSettings) { + const dexieSetting = dexieSettings.find(s => s.id === sqliteSetting.id); + if (!dexieSetting) { + missing.push(sqliteSetting); + } + } + + return { added, modified, missing }; +} + +/** + * Compares two contacts for equality + * + * This helper function performs a deep comparison of two Contact objects + * to determine if they are identical. The comparison includes all + * relevant fields including complex objects like contactMethods. + * + * For contactMethods, the function uses JSON.stringify to compare + * the arrays, ensuring that both structure and content are identical. + * + * @function contactsEqual + * @param {Contact} contact1 - First contact to compare + * @param {Contact} contact2 - Second contact to compare + * @returns {boolean} True if contacts are identical, false otherwise + * @example + * ```typescript + * const areEqual = contactsEqual(contact1, contact2); + * if (areEqual) { + * console.log('Contacts are identical'); + * } else { + * console.log('Contacts differ'); + * } + * ``` + */ +function contactsEqual(contact1: Contact, contact2: Contact): boolean { + return ( + contact1.did === contact2.did && + contact1.name === contact2.name && + contact1.notes === contact2.notes && + contact1.profileImageUrl === contact2.profileImageUrl && + contact1.publicKeyBase64 === contact2.publicKeyBase64 && + contact1.nextPubKeyHashB64 === contact2.nextPubKeyHashB64 && + contact1.seesMe === contact2.seesMe && + contact1.registered === contact2.registered && + JSON.stringify(contact1.contactMethods) === JSON.stringify(contact2.contactMethods) + ); +} + +/** + * Compares two settings for equality + * + * This helper function performs a deep comparison of two Settings objects + * to determine if they are identical. The comparison includes all + * relevant fields including complex objects like searchBoxes. + * + * For searchBoxes, the function uses JSON.stringify to compare + * the arrays, ensuring that both structure and content are identical. + * + * @function settingsEqual + * @param {Settings} settings1 - First settings to compare + * @param {Settings} settings2 - Second settings to compare + * @returns {boolean} True if settings are identical, false otherwise + * @example + * ```typescript + * const areEqual = settingsEqual(settings1, settings2); + * if (areEqual) { + * console.log('Settings are identical'); + * } else { + * console.log('Settings differ'); + * } + * ``` + */ +function settingsEqual(settings1: Settings, settings2: Settings): boolean { + return ( + settings1.id === settings2.id && + settings1.accountDid === settings2.accountDid && + settings1.activeDid === settings2.activeDid && + settings1.apiServer === settings2.apiServer && + settings1.filterFeedByNearby === settings2.filterFeedByNearby && + settings1.filterFeedByVisible === settings2.filterFeedByVisible && + settings1.finishedOnboarding === settings2.finishedOnboarding && + settings1.firstName === settings2.firstName && + settings1.hideRegisterPromptOnNewContact === settings2.hideRegisterPromptOnNewContact && + settings1.isRegistered === settings2.isRegistered && + settings1.lastName === settings2.lastName && + settings1.lastAckedOfferToUserJwtId === settings2.lastAckedOfferToUserJwtId && + settings1.lastAckedOfferToUserProjectsJwtId === settings2.lastAckedOfferToUserProjectsJwtId && + settings1.lastNotifiedClaimId === settings2.lastNotifiedClaimId && + settings1.lastViewedClaimId === settings2.lastViewedClaimId && + settings1.notifyingNewActivityTime === settings2.notifyingNewActivityTime && + settings1.notifyingReminderMessage === settings2.notifyingReminderMessage && + settings1.notifyingReminderTime === settings2.notifyingReminderTime && + settings1.partnerApiServer === settings2.partnerApiServer && + settings1.passkeyExpirationMinutes === settings2.passkeyExpirationMinutes && + settings1.profileImageUrl === settings2.profileImageUrl && + settings1.showContactGivesInline === settings2.showContactGivesInline && + settings1.showGeneralAdvanced === settings2.showGeneralAdvanced && + settings1.showShortcutBvc === settings2.showShortcutBvc && + settings1.vapid === settings2.vapid && + settings1.warnIfProdServer === settings2.warnIfProdServer && + settings1.warnIfTestServer === settings2.warnIfTestServer && + settings1.webPushServer === settings2.webPushServer && + JSON.stringify(settings1.searchBoxes) === JSON.stringify(settings2.searchBoxes) + ); +} + +/** + * Generates YAML-formatted comparison data + * + * This function converts the database comparison results into a + * structured format that can be exported and analyzed. The output + * is actually JSON but formatted in a YAML-like structure for + * better readability. + * + * The generated data includes summary statistics, detailed differences, + * and the actual data from both databases for inspection purposes. + * + * @function generateComparisonYaml + * @param {DataComparison} comparison - The comparison results to format + * @returns {string} JSON string formatted for readability + * @example + * ```typescript + * const comparison = await compareDatabases(); + * const yaml = generateComparisonYaml(comparison); + * console.log(yaml); + * // Save to file or display in UI + * ``` + */ +export function generateComparisonYaml(comparison: DataComparison): string { + const yaml = { + comparison: { + summary: { + dexieContacts: comparison.dexieContacts.length, + sqliteContacts: comparison.sqliteContacts.length, + dexieSettings: comparison.dexieSettings.length, + sqliteSettings: comparison.sqliteSettings.length, + }, + differences: { + contacts: { + added: comparison.differences.contacts.added.length, + modified: comparison.differences.contacts.modified.length, + missing: comparison.differences.contacts.missing.length, + }, + settings: { + added: comparison.differences.settings.added.length, + modified: comparison.differences.settings.modified.length, + missing: comparison.differences.settings.missing.length, + }, + }, + contacts: { + dexie: comparison.dexieContacts.map(c => ({ + did: c.did, + name: c.name, + notes: c.notes, + profileImageUrl: c.profileImageUrl, + seesMe: c.seesMe, + registered: c.registered, + contactMethods: c.contactMethods, + })), + sqlite: comparison.sqliteContacts.map(c => ({ + did: c.did, + name: c.name, + notes: c.notes, + profileImageUrl: c.profileImageUrl, + seesMe: c.seesMe, + registered: c.registered, + contactMethods: c.contactMethods, + })), + }, + settings: { + dexie: comparison.dexieSettings.map(s => ({ + id: s.id, + accountDid: s.accountDid, + activeDid: s.activeDid, + firstName: s.firstName, + isRegistered: s.isRegistered, + profileImageUrl: s.profileImageUrl, + showShortcutBvc: s.showShortcutBvc, + searchBoxes: s.searchBoxes, + })), + sqlite: comparison.sqliteSettings.map(s => ({ + id: s.id, + accountDid: s.accountDid, + activeDid: s.activeDid, + firstName: s.firstName, + isRegistered: s.isRegistered, + profileImageUrl: s.profileImageUrl, + showShortcutBvc: s.showShortcutBvc, + searchBoxes: s.searchBoxes, + })), + }, + }, + }; + + return JSON.stringify(yaml, null, 2); +} + +/** + * Migrates contacts from Dexie to SQLite database + * + * This function transfers all contacts from the Dexie database to the + * SQLite database. It handles both new contacts (INSERT) and existing + * contacts (UPDATE) based on the overwriteExisting parameter. + * + * The function processes contacts one by one to ensure data integrity + * and provides detailed logging of the migration process. It returns + * comprehensive results including success status, counts, and any + * errors or warnings encountered. + * + * @async + * @function migrateContacts + * @param {boolean} [overwriteExisting=false] - Whether to overwrite existing contacts in SQLite + * @returns {Promise} Detailed results of the migration operation + * @throws {Error} If the migration process fails completely + * @example + * ```typescript + * try { + * const result = await migrateContacts(true); // Overwrite existing + * if (result.success) { + * console.log(`Successfully migrated ${result.contactsMigrated} contacts`); + * } else { + * console.error('Migration failed:', result.errors); + * } + * } catch (error) { + * console.error('Migration process failed:', error); + * } + * ``` + */ +export async function migrateContacts(overwriteExisting: boolean = false): Promise { + logger.info("[MigrationService] Starting contact migration", { overwriteExisting }); + + const result: MigrationResult = { + success: true, + contactsMigrated: 0, + settingsMigrated: 0, + errors: [], + warnings: [], + }; + + try { + const dexieContacts = await getDexieContacts(); + const platformService = PlatformServiceFactory.getInstance(); + + for (const contact of dexieContacts) { + try { + // Check if contact already exists + const existingResult = await platformService.dbQuery( + "SELECT did FROM contacts WHERE did = ?", + [contact.did] ); + + if (existingResult?.values?.length) { + if (overwriteExisting) { + // Update existing contact + const { sql, params } = generateUpdateStatement( + contact as unknown as Record, + "contacts", + "did = ?", + [contact.did] + ); + await platformService.dbExec(sql, params); + result.contactsMigrated++; + logger.info(`[MigrationService] Updated contact: ${contact.did}`); + } else { + result.warnings.push(`Contact ${contact.did} already exists, skipping`); + } + } else { + // Insert new contact + const { sql, params } = generateInsertStatement( + contact as unknown as Record, + "contacts" + ); + await platformService.dbExec(sql, params); + result.contactsMigrated++; + logger.info(`[MigrationService] Added contact: ${contact.did}`); + } + } catch (error) { + const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`; + logger.error("[MigrationService]", errorMsg); + result.errors.push(errorMsg); + result.success = false; } } + + logger.info("[MigrationService] Contact migration completed", { + contactsMigrated: result.contactsMigrated, + errors: result.errors.length, + warnings: result.warnings.length, + }); + + return result; + } catch (error) { + const errorMsg = `Contact migration failed: ${error}`; + logger.error("[MigrationService]", errorMsg); + result.errors.push(errorMsg); + result.success = false; + return result; } } -export default MigrationService.getInstance(); +/** + * Migrates specific settings fields from Dexie to SQLite database + * + * This function transfers specific settings fields from the Dexie database + * to the SQLite database. It focuses on the most important user-facing + * settings: firstName, isRegistered, profileImageUrl, showShortcutBvc, + * and searchBoxes. + * + * The function handles both new settings (INSERT) and existing settings + * (UPDATE) based on the overwriteExisting parameter. For updates, it + * only modifies the specified fields, preserving other settings. + * + * @async + * @function migrateSettings + * @param {boolean} [overwriteExisting=false] - Whether to overwrite existing settings in SQLite + * @returns {Promise} Detailed results of the migration operation + * @throws {Error} If the migration process fails completely + * @example + * ```typescript + * try { + * const result = await migrateSettings(true); // Overwrite existing + * if (result.success) { + * console.log(`Successfully migrated ${result.settingsMigrated} settings`); + * } else { + * console.error('Migration failed:', result.errors); + * } + * } catch (error) { + * console.error('Migration process failed:', error); + * } + * ``` + */ +export async function migrateSettings(overwriteExisting: boolean = false): Promise { + logger.info("[MigrationService] Starting settings migration", { overwriteExisting }); + + const result: MigrationResult = { + success: true, + contactsMigrated: 0, + settingsMigrated: 0, + errors: [], + warnings: [], + }; + + try { + const dexieSettings = await getDexieSettings(); + const platformService = PlatformServiceFactory.getInstance(); + + // Fields to migrate - these are the most important user-facing settings + const fieldsToMigrate = ['firstName', 'isRegistered', 'profileImageUrl', 'showShortcutBvc', 'searchBoxes']; + + for (const dexieSetting of dexieSettings) { + try { + // Check if setting already exists + const existingResult = await platformService.dbQuery( + "SELECT id FROM settings WHERE id = ?", + [dexieSetting.id] + ); + + if (existingResult?.values?.length) { + if (overwriteExisting) { + // Update existing setting with only the specified fields + const updateData: Record = {}; + fieldsToMigrate.forEach(field => { + if (dexieSetting[field as keyof Settings] !== undefined) { + updateData[field] = dexieSetting[field as keyof Settings]; + } + }); + + if (Object.keys(updateData).length > 0) { + const { sql, params } = generateUpdateStatement( + updateData as unknown as Record, + "settings", + "id = ?", + [dexieSetting.id] + ); + await platformService.dbExec(sql, params); + result.settingsMigrated++; + logger.info(`[MigrationService] Updated settings: ${dexieSetting.id}`); + } + } else { + result.warnings.push(`Settings ${dexieSetting.id} already exists, skipping`); + } + } else { + // Insert new setting + const { sql, params } = generateInsertStatement( + dexieSetting as unknown as Record, + "settings" + ); + await platformService.dbExec(sql, params); + result.settingsMigrated++; + logger.info(`[MigrationService] Added settings: ${dexieSetting.id}`); + } + } catch (error) { + const errorMsg = `Failed to migrate settings ${dexieSetting.id}: ${error}`; + logger.error("[MigrationService]", errorMsg); + result.errors.push(errorMsg); + result.success = false; + } + } + + logger.info("[MigrationService] Settings migration completed", { + settingsMigrated: result.settingsMigrated, + errors: result.errors.length, + warnings: result.warnings.length, + }); + + return result; + } catch (error) { + const errorMsg = `Settings migration failed: ${error}`; + logger.error("[MigrationService]", errorMsg); + result.errors.push(errorMsg); + result.success = false; + return result; + } +} + +/** + * Generates SQL INSERT statement and parameters from a model object + * + * This helper function creates a parameterized SQL INSERT statement + * from a JavaScript object. It filters out undefined values and + * creates the appropriate SQL syntax with placeholders. + * + * The function is used internally by the migration functions to + * safely insert data into the SQLite database. + * + * @function generateInsertStatement + * @param {Record} model - The model object containing fields to insert + * @param {string} tableName - The name of the table to insert into + * @returns {Object} Object containing the SQL statement and parameters array + * @returns {string} returns.sql - The SQL INSERT statement + * @returns {unknown[]} returns.params - Array of parameter values + * @example + * ```typescript + * const contact = { did: 'did:example:123', name: 'John Doe' }; + * const { sql, params } = generateInsertStatement(contact, 'contacts'); + * // sql: "INSERT INTO contacts (did, name) VALUES (?, ?)" + * // params: ['did:example:123', 'John Doe'] + * ``` + */ +function generateInsertStatement( + model: Record, + tableName: string, +): { sql: string; params: unknown[] } { + const columns = Object.keys(model).filter((key) => model[key] !== undefined); + const values = Object.values(model).filter((value) => value !== undefined); + const placeholders = values.map(() => "?").join(", "); + const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`; + + return { + sql: insertSql, + params: values, + }; +} + +/** + * Generates SQL UPDATE statement and parameters from a model object + * + * This helper function creates a parameterized SQL UPDATE statement + * from a JavaScript object. It filters out undefined values and + * creates the appropriate SQL syntax with placeholders. + * + * The function is used internally by the migration functions to + * safely update data in the SQLite database. + * + * @function generateUpdateStatement + * @param {Record} model - The model object containing fields to update + * @param {string} tableName - The name of the table to update + * @param {string} whereClause - The WHERE clause for the update (e.g. "id = ?") + * @param {unknown[]} [whereParams=[]] - Parameters for the WHERE clause + * @returns {Object} Object containing the SQL statement and parameters array + * @returns {string} returns.sql - The SQL UPDATE statement + * @returns {unknown[]} returns.params - Array of parameter values + * @example + * ```typescript + * const contact = { name: 'Jane Doe' }; + * const { sql, params } = generateUpdateStatement(contact, 'contacts', 'did = ?', ['did:example:123']); + * // sql: "UPDATE contacts SET name = ? WHERE did = ?" + * // params: ['Jane Doe', 'did:example:123'] + * ``` + */ +function generateUpdateStatement( + model: Record, + tableName: string, + whereClause: string, + whereParams: unknown[] = [], +): { sql: string; params: unknown[] } { + const setClauses: string[] = []; + const params: unknown[] = []; + + Object.entries(model).forEach(([key, value]) => { + if (value !== undefined) { + setClauses.push(`${key} = ?`); + params.push(value); + } + }); + + if (setClauses.length === 0) { + throw new Error("No valid fields to update"); + } + + const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`; + + return { + sql, + params: [...params, ...whereParams], + }; +} From 40a2491d682687ac7ed9edff094dea8e1d79ab6d Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 18 Jun 2025 11:19:34 +0000 Subject: [PATCH 02/29] feat: Add comprehensive Database Migration UI component - Create DatabaseMigration.vue with vue-facing-decorator and Tailwind CSS - Add complete UI for comparing and migrating data between Dexie and SQLite - Implement real-time loading states, error handling, and success feedback - Add navigation link to Account page for easy access - Include export functionality for comparison data - Create comprehensive documentation in doc/database-migration-guide.md - Fix all linting issues and ensure code quality standards - Support both contact and settings migration with overwrite options - Add visual difference analysis with summary cards and detailed breakdowns The component provides a professional interface for the migrationService.ts, enabling users to safely transfer data between database systems during the transition from Dexie to SQLite storage. --- doc/database-migration-guide.md | 295 +++++++++++ src/router/index.ts | 5 + src/services/migrationService.ts | 234 +++++---- src/views/AccountViewView.vue | 6 + src/views/DatabaseMigration.vue | 860 +++++++++++++++++++++++++++++++ 5 files changed, 1305 insertions(+), 95 deletions(-) create mode 100644 doc/database-migration-guide.md create mode 100644 src/views/DatabaseMigration.vue diff --git a/doc/database-migration-guide.md b/doc/database-migration-guide.md new file mode 100644 index 00000000..fe17287e --- /dev/null +++ b/doc/database-migration-guide.md @@ -0,0 +1,295 @@ +# Database Migration Guide + +## Overview + +The Database Migration feature allows you to compare and migrate data between Dexie (IndexedDB) and SQLite databases in the TimeSafari application. This is particularly useful during the transition from the old Dexie-based storage system to the new SQLite-based system. + +## Features + +### 1. Database Comparison + +- Compare data between Dexie and SQLite databases +- View detailed differences in contacts and settings +- Identify added, modified, and missing records +- Export comparison results for analysis + +### 2. Data Migration + +- Migrate contacts from Dexie to SQLite +- Migrate settings from Dexie to SQLite +- Option to overwrite existing records or skip them +- Comprehensive error handling and reporting + +### 3. User Interface + +- Modern, responsive UI built with Tailwind CSS +- Real-time loading states and progress indicators +- Clear success and error messaging +- Export functionality for comparison data + +## Prerequisites + +### Enable Dexie Database + +Before using the migration features, you must enable the Dexie database by setting: + +```typescript +// In constants/app.ts +export const USE_DEXIE_DB = true; +``` + +**Note**: This should only be enabled temporarily during migration. Remember to set it back to `false` after migration is complete. + +## Accessing the Migration Interface + +1. Navigate to the **Account** page in the TimeSafari app +2. Scroll down to find the **Database Migration** link +3. Click the link to open the migration interface + +## Using the Migration Interface + +### Step 1: Compare Databases + +1. Click the **"Compare Databases"** button +2. The system will retrieve data from both Dexie and SQLite databases +3. Review the comparison results showing: + - Summary counts for each database + - Detailed differences (added, modified, missing records) + - Specific records that need attention + +### Step 2: Review Differences + +The comparison results are displayed in several sections: + +#### Summary Cards + +- **Dexie Contacts**: Number of contacts in Dexie database +- **SQLite Contacts**: Number of contacts in SQLite database +- **Dexie Settings**: Number of settings in Dexie database +- **SQLite Settings**: Number of settings in SQLite database + +#### Contact Differences + +- **Added**: Contacts in Dexie but not in SQLite +- **Modified**: Contacts that differ between databases +- **Missing**: Contacts in SQLite but not in Dexie + +#### Settings Differences + +- **Added**: Settings in Dexie but not in SQLite +- **Modified**: Settings that differ between databases +- **Missing**: Settings in SQLite but not in Dexie + +### Step 3: Configure Migration Options + +Before migrating data, configure the migration options: + +- **Overwrite existing records**: When enabled, existing records in SQLite will be updated with data from Dexie. When disabled, existing records will be skipped. + +### Step 4: Migrate Data + +#### Migrate Contacts + +1. Click the **"Migrate Contacts"** button +2. The system will transfer contacts from Dexie to SQLite +3. Review the migration results showing: + - Number of contacts successfully migrated + - Any warnings or errors encountered + +#### Migrate Settings + +1. Click the **"Migrate Settings"** button +2. The system will transfer settings from Dexie to SQLite +3. Review the migration results showing: + - Number of settings successfully migrated + - Any warnings or errors encountered + +### Step 5: Export Comparison (Optional) + +1. Click the **"Export Comparison"** button +2. A JSON file will be downloaded containing the complete comparison data +3. This file can be used for analysis or backup purposes + +## Migration Process Details + +### Contact Migration + +The contact migration process: + +1. **Retrieves** all contacts from Dexie database +2. **Checks** for existing contacts in SQLite by DID +3. **Inserts** new contacts or **updates** existing ones (if overwrite is enabled) +4. **Handles** complex fields like `contactMethods` (JSON arrays) +5. **Reports** success/failure for each contact + +### Settings Migration + +The settings migration process: + +1. **Retrieves** all settings from Dexie database +2. **Focuses** on key user-facing settings: + - `firstName` + - `isRegistered` + - `profileImageUrl` + - `showShortcutBvc` + - `searchBoxes` +3. **Preserves** other settings in SQLite +4. **Reports** success/failure for each setting + +## Error Handling + +### Common Issues + +#### Dexie Database Not Enabled + +**Error**: "Dexie database is not enabled" +**Solution**: Set `USE_DEXIE_DB = true` in `constants/app.ts` + +#### Database Connection Issues + +**Error**: "Failed to retrieve Dexie contacts" +**Solution**: Check that the Dexie database is properly initialized and accessible + +#### SQLite Query Errors + +**Error**: "Failed to retrieve SQLite contacts" +**Solution**: Verify that the SQLite database is properly set up and the platform service is working + +#### Migration Failures + +**Error**: "Migration failed: [specific error]" +**Solution**: Review the error details and check data integrity in both databases + +### Error Recovery + +1. **Review** the error messages carefully +2. **Check** the browser console for additional details +3. **Verify** database connectivity and permissions +4. **Retry** the operation if appropriate +5. **Export** comparison data for manual review if needed + +## Best Practices + +### Before Migration + +1. **Backup** your data if possible +2. **Test** the migration on a small dataset first +3. **Verify** that both databases are accessible +4. **Review** the comparison results before migrating + +### During Migration + +1. **Don't** interrupt the migration process +2. **Monitor** the progress and error messages +3. **Note** any warnings or skipped records +4. **Export** comparison data for reference + +### After Migration + +1. **Verify** that data was migrated correctly +2. **Test** the application functionality +3. **Disable** Dexie database (`USE_DEXIE_DB = false`) +4. **Clean up** any temporary files or exports + +## Technical Details + +### Database Schema + +The migration handles the following data structures: + +#### Contacts Table + +```typescript +interface Contact { + did: string; // Decentralized Identifier + name: string; // Contact name + contactMethods: ContactMethod[]; // Array of contact methods + nextPubKeyHashB64: string; // Next public key hash + notes: string; // Contact notes + profileImageUrl: string; // Profile image URL + publicKeyBase64: string; // Public key in base64 + seesMe: boolean; // Visibility flag + registered: boolean; // Registration status +} +``` + +#### Settings Table + +```typescript +interface Settings { + id: number; // Settings ID + accountDid: string; // Account DID + activeDid: string; // Active DID + firstName: string; // User's first name + isRegistered: boolean; // Registration status + profileImageUrl: string; // Profile image URL + showShortcutBvc: boolean; // UI preference + searchBoxes: any[]; // Search configuration + // ... other fields +} +``` + +### Migration Logic + +The migration service uses sophisticated comparison logic: + +1. **Primary Key Matching**: Uses DID for contacts, ID for settings +2. **Deep Comparison**: Compares all fields including complex objects +3. **JSON Handling**: Properly handles JSON fields like `contactMethods` and `searchBoxes` +4. **Conflict Resolution**: Provides options for handling existing records + +### Performance Considerations + +- **Batch Processing**: Processes records one by one for reliability +- **Error Isolation**: Individual record failures don't stop the entire migration +- **Memory Management**: Handles large datasets efficiently +- **Progress Reporting**: Provides real-time feedback during migration + +## Troubleshooting + +### Migration Stuck + +If the migration appears to be stuck: + +1. **Check** the browser console for errors +2. **Refresh** the page and try again +3. **Verify** database connectivity +4. **Check** for large datasets that might take time + +### Incomplete Migration + +If migration doesn't complete: + +1. **Review** error messages +2. **Check** data integrity in both databases +3. **Export** comparison data for manual review +4. **Consider** migrating in smaller batches + +### Data Inconsistencies + +If you notice data inconsistencies: + +1. **Export** comparison data +2. **Review** the differences carefully +3. **Manually** verify critical records +4. **Consider** selective migration of specific records + +## Support + +For issues with the Database Migration feature: + +1. **Check** this documentation first +2. **Review** the browser console for error details +3. **Export** comparison data for analysis +4. **Contact** the development team with specific error details + +## Security Considerations + +- **Data Privacy**: Migration data is processed locally and not sent to external servers +- **Access Control**: Only users with access to the account can perform migration +- **Data Integrity**: Migration preserves data integrity and handles conflicts gracefully +- **Audit Trail**: Export functionality provides an audit trail of migration operations + +--- + +**Note**: This migration tool is designed for the transition period between database systems. Once migration is complete and verified, the Dexie database should be disabled to avoid confusion and potential data conflicts. diff --git a/src/router/index.ts b/src/router/index.ts index 0b9aa52b..1c2b0d32 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -143,6 +143,11 @@ const routes: Array = [ name: "logs", component: () => import("../views/LogView.vue"), }, + { + path: "/database-migration", + name: "database-migration", + component: () => import("../views/DatabaseMigration.vue"), + }, { path: "/new-activity", name: "new-activity", diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index c8dc4e84..fd08cba5 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -1,21 +1,21 @@ /** * Migration Service for transferring data from Dexie (IndexedDB) to SQLite - * + * * This service provides functions to: * 1. Compare data between Dexie and SQLite databases * 2. Transfer contacts and settings from Dexie to SQLite * 3. Generate YAML-formatted data comparisons - * + * * The service is designed to work with the TimeSafari app's dual database architecture, * where data can exist in both Dexie (IndexedDB) and SQLite databases. This allows * for safe migration of data between the two storage systems. - * + * * Usage: * 1. Enable Dexie temporarily by setting USE_DEXIE_DB = true in constants/app.ts * 2. Use compareDatabases() to see differences between databases * 3. Use migrateContacts() and/or migrateSettings() to transfer data * 4. Disable Dexie again after migration is complete - * + * * @author Matthew Raymer * @version 1.0.0 * @since 2024 @@ -31,11 +31,11 @@ import { USE_DEXIE_DB } from "../constants/app"; /** * Interface for data comparison results between Dexie and SQLite databases - * + * * This interface provides a comprehensive view of the differences between * the two database systems, including counts and detailed lists of * added, modified, and missing records. - * + * * @interface DataComparison * @property {Contact[]} dexieContacts - All contacts from Dexie database * @property {Contact[]} sqliteContacts - All contacts from SQLite database @@ -72,11 +72,11 @@ export interface DataComparison { /** * Interface for migration operation results - * + * * Provides detailed feedback about the success or failure of migration * operations, including counts of migrated records and any errors or * warnings that occurred during the process. - * + * * @interface MigrationResult * @property {boolean} success - Whether the migration operation completed successfully * @property {number} contactsMigrated - Number of contacts successfully migrated @@ -94,13 +94,13 @@ export interface MigrationResult { /** * Retrieves all contacts from the Dexie (IndexedDB) database - * + * * This function connects to the Dexie database and retrieves all contact * records. It requires that USE_DEXIE_DB is enabled in the app constants. - * + * * The function handles database opening and error conditions, providing * detailed logging for debugging purposes. - * + * * @async * @function getDexieContacts * @returns {Promise} Array of all contacts from Dexie database @@ -123,7 +123,9 @@ export async function getDexieContacts(): Promise { try { await db.open(); const contacts = await db.contacts.toArray(); - logger.info(`[MigrationService] Retrieved ${contacts.length} contacts from Dexie`); + logger.info( + `[MigrationService] Retrieved ${contacts.length} contacts from Dexie`, + ); return contacts; } catch (error) { logger.error("[MigrationService] Error retrieving Dexie contacts:", error); @@ -133,14 +135,14 @@ export async function getDexieContacts(): Promise { /** * Retrieves all contacts from the SQLite database - * + * * This function uses the platform service to query the SQLite database * and retrieve all contact records. It handles the conversion of raw * database results into properly typed Contact objects. - * + * * The function also handles JSON parsing for complex fields like * contactMethods, ensuring proper type conversion. - * + * * @async * @function getSqliteContacts * @returns {Promise} Array of all contacts from SQLite database @@ -159,7 +161,7 @@ export async function getSqliteContacts(): Promise { try { const platformService = PlatformServiceFactory.getInstance(); const result = await platformService.dbQuery("SELECT * FROM contacts"); - + if (!result?.values?.length) { return []; } @@ -169,7 +171,10 @@ export async function getSqliteContacts(): Promise { return { did: contact.did || "", name: contact.name || "", - contactMethods: parseJsonField(contact.contactMethods, []) as ContactMethod[], + contactMethods: parseJsonField( + contact.contactMethods, + [], + ) as ContactMethod[], nextPubKeyHashB64: contact.nextPubKeyHashB64 || "", notes: contact.notes || "", profileImageUrl: contact.profileImageUrl || "", @@ -179,7 +184,9 @@ export async function getSqliteContacts(): Promise { } as Contact; }); - logger.info(`[MigrationService] Retrieved ${contacts.length} contacts from SQLite`); + logger.info( + `[MigrationService] Retrieved ${contacts.length} contacts from SQLite`, + ); return contacts; } catch (error) { logger.error("[MigrationService] Error retrieving SQLite contacts:", error); @@ -189,13 +196,13 @@ export async function getSqliteContacts(): Promise { /** * Retrieves all settings from the Dexie (IndexedDB) database - * + * * This function connects to the Dexie database and retrieves all settings * records. It requires that USE_DEXIE_DB is enabled in the app constants. - * + * * Settings include both master settings (id=1) and account-specific settings * that override the master settings for particular user accounts. - * + * * @async * @function getDexieSettings * @returns {Promise} Array of all settings from Dexie database @@ -218,7 +225,9 @@ export async function getDexieSettings(): Promise { try { await db.open(); const settings = await db.settings.toArray(); - logger.info(`[MigrationService] Retrieved ${settings.length} settings from Dexie`); + logger.info( + `[MigrationService] Retrieved ${settings.length} settings from Dexie`, + ); return settings; } catch (error) { logger.error("[MigrationService] Error retrieving Dexie settings:", error); @@ -228,14 +237,14 @@ export async function getDexieSettings(): Promise { /** * Retrieves all settings from the SQLite database - * + * * This function uses the platform service to query the SQLite database * and retrieve all settings records. It handles the conversion of raw * database results into properly typed Settings objects. - * + * * The function also handles JSON parsing for complex fields like * searchBoxes, ensuring proper type conversion. - * + * * @async * @function getSqliteSettings * @returns {Promise} Array of all settings from SQLite database @@ -254,7 +263,7 @@ export async function getSqliteSettings(): Promise { try { const platformService = PlatformServiceFactory.getInstance(); const result = await platformService.dbQuery("SELECT * FROM settings"); - + if (!result?.values?.length) { return []; } @@ -270,11 +279,13 @@ export async function getSqliteSettings(): Promise { filterFeedByVisible: setting.filterFeedByVisible || false, finishedOnboarding: setting.finishedOnboarding || false, firstName: setting.firstName || "", - hideRegisterPromptOnNewContact: setting.hideRegisterPromptOnNewContact || false, + hideRegisterPromptOnNewContact: + setting.hideRegisterPromptOnNewContact || false, isRegistered: setting.isRegistered || false, lastName: setting.lastName || "", lastAckedOfferToUserJwtId: setting.lastAckedOfferToUserJwtId || "", - lastAckedOfferToUserProjectsJwtId: setting.lastAckedOfferToUserProjectsJwtId || "", + lastAckedOfferToUserProjectsJwtId: + setting.lastAckedOfferToUserProjectsJwtId || "", lastNotifiedClaimId: setting.lastNotifiedClaimId || "", lastViewedClaimId: setting.lastViewedClaimId || "", notifyingNewActivityTime: setting.notifyingNewActivityTime || "", @@ -294,7 +305,9 @@ export async function getSqliteSettings(): Promise { } as Settings; }); - logger.info(`[MigrationService] Retrieved ${settings.length} settings from SQLite`); + logger.info( + `[MigrationService] Retrieved ${settings.length} settings from SQLite`, + ); return settings; } catch (error) { logger.error("[MigrationService] Error retrieving SQLite settings:", error); @@ -304,16 +317,16 @@ export async function getSqliteSettings(): Promise { /** * Compares data between Dexie and SQLite databases - * + * * This is the main comparison function that retrieves data from both * databases and identifies differences. It provides a comprehensive * view of what data exists in each database and what needs to be * migrated. - * + * * The function performs parallel data retrieval for efficiency and * then compares the results to identify added, modified, and missing * records in each table. - * + * * @async * @function compareDatabases * @returns {Promise} Comprehensive comparison results @@ -333,16 +346,17 @@ export async function getSqliteSettings(): Promise { export async function compareDatabases(): Promise { logger.info("[MigrationService] Starting database comparison"); - const [dexieContacts, sqliteContacts, dexieSettings, sqliteSettings] = await Promise.all([ - getDexieContacts(), - getSqliteContacts(), - getDexieSettings(), - getSqliteSettings(), - ]); + const [dexieContacts, sqliteContacts, dexieSettings, sqliteSettings] = + await Promise.all([ + getDexieContacts(), + getSqliteContacts(), + getDexieSettings(), + getSqliteSettings(), + ]); // Compare contacts const contactDifferences = compareContacts(dexieContacts, sqliteContacts); - + // Compare settings const settingsDifferences = compareSettings(dexieSettings, sqliteSettings); @@ -371,15 +385,15 @@ export async function compareDatabases(): Promise { /** * Compares contacts between Dexie and SQLite databases - * + * * This helper function analyzes two arrays of contacts and identifies * which contacts are added (in Dexie but not SQLite), modified * (different between databases), or missing (in SQLite but not Dexie). - * + * * The comparison is based on the contact's DID (Decentralized Identifier) * as the primary key, with detailed field-by-field comparison for * modified contacts. - * + * * @function compareContacts * @param {Contact[]} dexieContacts - Contacts from Dexie database * @param {Contact[]} sqliteContacts - Contacts from SQLite database @@ -402,7 +416,9 @@ function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) { // Find contacts that exist in Dexie but not in SQLite for (const dexieContact of dexieContacts) { - const sqliteContact = sqliteContacts.find(c => c.did === dexieContact.did); + const sqliteContact = sqliteContacts.find( + (c) => c.did === dexieContact.did, + ); if (!sqliteContact) { added.push(dexieContact); } else if (!contactsEqual(dexieContact, sqliteContact)) { @@ -412,7 +428,7 @@ function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) { // Find contacts that exist in SQLite but not in Dexie for (const sqliteContact of sqliteContacts) { - const dexieContact = dexieContacts.find(c => c.did === sqliteContact.did); + const dexieContact = dexieContacts.find((c) => c.did === sqliteContact.did); if (!dexieContact) { missing.push(sqliteContact); } @@ -423,14 +439,14 @@ function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) { /** * Compares settings between Dexie and SQLite databases - * + * * This helper function analyzes two arrays of settings and identifies * which settings are added (in Dexie but not SQLite), modified * (different between databases), or missing (in SQLite but not Dexie). - * + * * The comparison is based on the setting's ID as the primary key, * with detailed field-by-field comparison for modified settings. - * + * * @function compareSettings * @param {Settings[]} dexieSettings - Settings from Dexie database * @param {Settings[]} sqliteSettings - Settings from SQLite database @@ -446,14 +462,17 @@ function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) { * console.log(`Missing: ${differences.missing.length}`); * ``` */ -function compareSettings(dexieSettings: Settings[], sqliteSettings: Settings[]) { +function compareSettings( + dexieSettings: Settings[], + sqliteSettings: Settings[], +) { const added: Settings[] = []; const modified: Settings[] = []; const missing: Settings[] = []; // Find settings that exist in Dexie but not in SQLite for (const dexieSetting of dexieSettings) { - const sqliteSetting = sqliteSettings.find(s => s.id === dexieSetting.id); + const sqliteSetting = sqliteSettings.find((s) => s.id === dexieSetting.id); if (!sqliteSetting) { added.push(dexieSetting); } else if (!settingsEqual(dexieSetting, sqliteSetting)) { @@ -463,7 +482,7 @@ function compareSettings(dexieSettings: Settings[], sqliteSettings: Settings[]) // Find settings that exist in SQLite but not in Dexie for (const sqliteSetting of sqliteSettings) { - const dexieSetting = dexieSettings.find(s => s.id === sqliteSetting.id); + const dexieSetting = dexieSettings.find((s) => s.id === sqliteSetting.id); if (!dexieSetting) { missing.push(sqliteSetting); } @@ -474,14 +493,14 @@ function compareSettings(dexieSettings: Settings[], sqliteSettings: Settings[]) /** * Compares two contacts for equality - * + * * This helper function performs a deep comparison of two Contact objects * to determine if they are identical. The comparison includes all * relevant fields including complex objects like contactMethods. - * + * * For contactMethods, the function uses JSON.stringify to compare * the arrays, ensuring that both structure and content are identical. - * + * * @function contactsEqual * @param {Contact} contact1 - First contact to compare * @param {Contact} contact2 - Second contact to compare @@ -506,20 +525,21 @@ function contactsEqual(contact1: Contact, contact2: Contact): boolean { contact1.nextPubKeyHashB64 === contact2.nextPubKeyHashB64 && contact1.seesMe === contact2.seesMe && contact1.registered === contact2.registered && - JSON.stringify(contact1.contactMethods) === JSON.stringify(contact2.contactMethods) + JSON.stringify(contact1.contactMethods) === + JSON.stringify(contact2.contactMethods) ); } /** * Compares two settings for equality - * + * * This helper function performs a deep comparison of two Settings objects * to determine if they are identical. The comparison includes all * relevant fields including complex objects like searchBoxes. - * + * * For searchBoxes, the function uses JSON.stringify to compare * the arrays, ensuring that both structure and content are identical. - * + * * @function settingsEqual * @param {Settings} settings1 - First settings to compare * @param {Settings} settings2 - Second settings to compare @@ -544,11 +564,14 @@ function settingsEqual(settings1: Settings, settings2: Settings): boolean { settings1.filterFeedByVisible === settings2.filterFeedByVisible && settings1.finishedOnboarding === settings2.finishedOnboarding && settings1.firstName === settings2.firstName && - settings1.hideRegisterPromptOnNewContact === settings2.hideRegisterPromptOnNewContact && + settings1.hideRegisterPromptOnNewContact === + settings2.hideRegisterPromptOnNewContact && settings1.isRegistered === settings2.isRegistered && settings1.lastName === settings2.lastName && - settings1.lastAckedOfferToUserJwtId === settings2.lastAckedOfferToUserJwtId && - settings1.lastAckedOfferToUserProjectsJwtId === settings2.lastAckedOfferToUserProjectsJwtId && + settings1.lastAckedOfferToUserJwtId === + settings2.lastAckedOfferToUserJwtId && + settings1.lastAckedOfferToUserProjectsJwtId === + settings2.lastAckedOfferToUserProjectsJwtId && settings1.lastNotifiedClaimId === settings2.lastNotifiedClaimId && settings1.lastViewedClaimId === settings2.lastViewedClaimId && settings1.notifyingNewActivityTime === settings2.notifyingNewActivityTime && @@ -564,21 +587,22 @@ function settingsEqual(settings1: Settings, settings2: Settings): boolean { settings1.warnIfProdServer === settings2.warnIfProdServer && settings1.warnIfTestServer === settings2.warnIfTestServer && settings1.webPushServer === settings2.webPushServer && - JSON.stringify(settings1.searchBoxes) === JSON.stringify(settings2.searchBoxes) + JSON.stringify(settings1.searchBoxes) === + JSON.stringify(settings2.searchBoxes) ); } /** * Generates YAML-formatted comparison data - * + * * This function converts the database comparison results into a * structured format that can be exported and analyzed. The output * is actually JSON but formatted in a YAML-like structure for * better readability. - * + * * The generated data includes summary statistics, detailed differences, * and the actual data from both databases for inspection purposes. - * + * * @function generateComparisonYaml * @param {DataComparison} comparison - The comparison results to format * @returns {string} JSON string formatted for readability @@ -612,7 +636,7 @@ export function generateComparisonYaml(comparison: DataComparison): string { }, }, contacts: { - dexie: comparison.dexieContacts.map(c => ({ + dexie: comparison.dexieContacts.map((c) => ({ did: c.did, name: c.name, notes: c.notes, @@ -621,7 +645,7 @@ export function generateComparisonYaml(comparison: DataComparison): string { registered: c.registered, contactMethods: c.contactMethods, })), - sqlite: comparison.sqliteContacts.map(c => ({ + sqlite: comparison.sqliteContacts.map((c) => ({ did: c.did, name: c.name, notes: c.notes, @@ -632,7 +656,7 @@ export function generateComparisonYaml(comparison: DataComparison): string { })), }, settings: { - dexie: comparison.dexieSettings.map(s => ({ + dexie: comparison.dexieSettings.map((s) => ({ id: s.id, accountDid: s.accountDid, activeDid: s.activeDid, @@ -642,7 +666,7 @@ export function generateComparisonYaml(comparison: DataComparison): string { showShortcutBvc: s.showShortcutBvc, searchBoxes: s.searchBoxes, })), - sqlite: comparison.sqliteSettings.map(s => ({ + sqlite: comparison.sqliteSettings.map((s) => ({ id: s.id, accountDid: s.accountDid, activeDid: s.activeDid, @@ -661,16 +685,16 @@ export function generateComparisonYaml(comparison: DataComparison): string { /** * Migrates contacts from Dexie to SQLite database - * + * * This function transfers all contacts from the Dexie database to the * SQLite database. It handles both new contacts (INSERT) and existing * contacts (UPDATE) based on the overwriteExisting parameter. - * + * * The function processes contacts one by one to ensure data integrity * and provides detailed logging of the migration process. It returns * comprehensive results including success status, counts, and any * errors or warnings encountered. - * + * * @async * @function migrateContacts * @param {boolean} [overwriteExisting=false] - Whether to overwrite existing contacts in SQLite @@ -690,8 +714,12 @@ export function generateComparisonYaml(comparison: DataComparison): string { * } * ``` */ -export async function migrateContacts(overwriteExisting: boolean = false): Promise { - logger.info("[MigrationService] Starting contact migration", { overwriteExisting }); +export async function migrateContacts( + overwriteExisting: boolean = false, +): Promise { + logger.info("[MigrationService] Starting contact migration", { + overwriteExisting, + }); const result: MigrationResult = { success: true, @@ -710,7 +738,7 @@ export async function migrateContacts(overwriteExisting: boolean = false): Promi // Check if contact already exists const existingResult = await platformService.dbQuery( "SELECT did FROM contacts WHERE did = ?", - [contact.did] + [contact.did], ); if (existingResult?.values?.length) { @@ -720,19 +748,21 @@ export async function migrateContacts(overwriteExisting: boolean = false): Promi contact as unknown as Record, "contacts", "did = ?", - [contact.did] + [contact.did], ); await platformService.dbExec(sql, params); result.contactsMigrated++; logger.info(`[MigrationService] Updated contact: ${contact.did}`); } else { - result.warnings.push(`Contact ${contact.did} already exists, skipping`); + result.warnings.push( + `Contact ${contact.did} already exists, skipping`, + ); } } else { // Insert new contact const { sql, params } = generateInsertStatement( contact as unknown as Record, - "contacts" + "contacts", ); await platformService.dbExec(sql, params); result.contactsMigrated++; @@ -764,16 +794,16 @@ export async function migrateContacts(overwriteExisting: boolean = false): Promi /** * Migrates specific settings fields from Dexie to SQLite database - * + * * This function transfers specific settings fields from the Dexie database * to the SQLite database. It focuses on the most important user-facing * settings: firstName, isRegistered, profileImageUrl, showShortcutBvc, * and searchBoxes. - * + * * The function handles both new settings (INSERT) and existing settings * (UPDATE) based on the overwriteExisting parameter. For updates, it * only modifies the specified fields, preserving other settings. - * + * * @async * @function migrateSettings * @param {boolean} [overwriteExisting=false] - Whether to overwrite existing settings in SQLite @@ -793,8 +823,12 @@ export async function migrateContacts(overwriteExisting: boolean = false): Promi * } * ``` */ -export async function migrateSettings(overwriteExisting: boolean = false): Promise { - logger.info("[MigrationService] Starting settings migration", { overwriteExisting }); +export async function migrateSettings( + overwriteExisting: boolean = false, +): Promise { + logger.info("[MigrationService] Starting settings migration", { + overwriteExisting, + }); const result: MigrationResult = { success: true, @@ -809,21 +843,27 @@ export async function migrateSettings(overwriteExisting: boolean = false): Promi const platformService = PlatformServiceFactory.getInstance(); // Fields to migrate - these are the most important user-facing settings - const fieldsToMigrate = ['firstName', 'isRegistered', 'profileImageUrl', 'showShortcutBvc', 'searchBoxes']; + const fieldsToMigrate = [ + "firstName", + "isRegistered", + "profileImageUrl", + "showShortcutBvc", + "searchBoxes", + ]; for (const dexieSetting of dexieSettings) { try { // Check if setting already exists const existingResult = await platformService.dbQuery( "SELECT id FROM settings WHERE id = ?", - [dexieSetting.id] + [dexieSetting.id], ); if (existingResult?.values?.length) { if (overwriteExisting) { // Update existing setting with only the specified fields const updateData: Record = {}; - fieldsToMigrate.forEach(field => { + fieldsToMigrate.forEach((field) => { if (dexieSetting[field as keyof Settings] !== undefined) { updateData[field] = dexieSetting[field as keyof Settings]; } @@ -834,20 +874,24 @@ export async function migrateSettings(overwriteExisting: boolean = false): Promi updateData as unknown as Record, "settings", "id = ?", - [dexieSetting.id] + [dexieSetting.id], ); await platformService.dbExec(sql, params); result.settingsMigrated++; - logger.info(`[MigrationService] Updated settings: ${dexieSetting.id}`); + logger.info( + `[MigrationService] Updated settings: ${dexieSetting.id}`, + ); } } else { - result.warnings.push(`Settings ${dexieSetting.id} already exists, skipping`); + result.warnings.push( + `Settings ${dexieSetting.id} already exists, skipping`, + ); } } else { // Insert new setting const { sql, params } = generateInsertStatement( dexieSetting as unknown as Record, - "settings" + "settings", ); await platformService.dbExec(sql, params); result.settingsMigrated++; @@ -879,14 +923,14 @@ export async function migrateSettings(overwriteExisting: boolean = false): Promi /** * Generates SQL INSERT statement and parameters from a model object - * + * * This helper function creates a parameterized SQL INSERT statement * from a JavaScript object. It filters out undefined values and * creates the appropriate SQL syntax with placeholders. - * + * * The function is used internally by the migration functions to * safely insert data into the SQLite database. - * + * * @function generateInsertStatement * @param {Record} model - The model object containing fields to insert * @param {string} tableName - The name of the table to insert into @@ -918,14 +962,14 @@ function generateInsertStatement( /** * Generates SQL UPDATE statement and parameters from a model object - * + * * This helper function creates a parameterized SQL UPDATE statement * from a JavaScript object. It filters out undefined values and * creates the appropriate SQL syntax with placeholders. - * + * * The function is used internally by the migration functions to * safely update data in the SQLite database. - * + * * @function generateUpdateStatement * @param {Record} model - The model object containing fields to update * @param {string} tableName - The name of the table to update diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index c3fee747..896b22fa 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -953,6 +953,12 @@ > Logs + + Database Migration + +
+
+ +
+

Database Migration

+

+ Compare and migrate data between Dexie (IndexedDB) and SQLite + databases +

+
+ + +
+
+
+ + + +
+
+

+ Dexie Database Disabled +

+
+

+ To use migration features, enable Dexie database by setting + USE_DEXIE_DB = true + in + constants/app.ts +

+
+
+
+
+ + +
+ + + + + + + +
+ + +
+
+ + + + + {{ loadingMessage }} +
+
+ + +
+
+
+ + + +
+
+

Error

+
+

{{ error }}

+
+
+
+
+ + +
+
+
+ + + +
+
+

Success

+
+

{{ successMessage }}

+
+
+
+
+ + +
+ +
+
+
+
+
+ + + +
+
+
+
+ Dexie Contacts +
+
+ {{ comparison.dexieContacts.length }} +
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+
+ SQLite Contacts +
+
+ {{ comparison.sqliteContacts.length }} +
+
+
+
+
+
+ +
+
+
+
+ + + + +
+
+
+
+ Dexie Settings +
+
+ {{ comparison.dexieSettings.length }} +
+
+
+
+
+
+ +
+
+
+
+ + + +
+
+
+
+ SQLite Settings +
+
+ {{ comparison.sqliteSettings.length }} +
+
+
+
+
+
+
+ + +
+ +
+
+

+ Contact Differences +

+ +
+
+
+ + + + Added +
+ {{ + comparison.differences.contacts.added.length + }} +
+ +
+
+ + + + Modified +
+ {{ + comparison.differences.contacts.modified.length + }} +
+ +
+
+ + + + Missing +
+ {{ + comparison.differences.contacts.missing.length + }} +
+
+ + +
+

+ Added Contacts: +

+
+
+ {{ contact.name || "Unnamed" }} ({{ + contact.did.substring(0, 20) + }}...) +
+
+
+
+
+ + +
+
+

+ Settings Differences +

+ +
+
+
+ + + + Added +
+ {{ + comparison.differences.settings.added.length + }} +
+ +
+
+ + + + Modified +
+ {{ + comparison.differences.settings.modified.length + }} +
+ +
+
+ + + + Missing +
+ {{ + comparison.differences.settings.missing.length + }} +
+
+ + +
+

+ Added Settings: +

+
+
+ ID: {{ setting.id }} - {{ setting.firstName || "Unnamed" }} +
+
+
+
+
+
+ + +
+
+

+ Migration Options +

+ +
+
+ + +
+ +

+ When enabled, existing records in SQLite will be updated with + data from Dexie. When disabled, existing records will be skipped + during migration. +

+
+
+
+
+
+
+ + + From f375a4e11ab1e192f78bb20ed0827cbeefd4b886 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 19 Jun 2025 05:51:59 +0000 Subject: [PATCH 03/29] feat: move database migration link from account view to start view - Remove database migration link from AccountViewView.vue - Add new "Database Tools" section to StartView.vue - Improve user flow by making database tools accessible from start page - Maintain consistent styling and functionality - Clean up account view to focus on account-specific settings The database migration feature is now logically grouped with other identity-related operations and more discoverable for users. --- src/libs/util.ts | 36 ++++++++++++++++++++++++++++ src/views/AccountViewView.vue | 6 ----- src/views/ImportAccountView.vue | 42 ++++++--------------------------- src/views/StartView.vue | 13 ++++++++++ 4 files changed, 56 insertions(+), 41 deletions(-) diff --git a/src/libs/util.ts b/src/libs/util.ts index 03ee7637..030ba416 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -45,6 +45,7 @@ import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { sha256 } from "ethereum-cryptography/sha256"; import { IIdentifier } from "@veramo/core"; import { insertDidSpecificSettings, parseJsonField } from "../db/databaseUtil"; +import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto"; export interface GiverReceiverInputInfo { did?: string; @@ -998,3 +999,38 @@ export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => { }, }; }; + +/** + * Imports an account from a mnemonic phrase + * @param mnemonic - The seed phrase to import from + * @param derivationPath - The derivation path to use (defaults to DEFAULT_ROOT_DERIVATION_PATH) + * @param shouldErase - Whether to erase existing accounts before importing + * @returns Promise that resolves when import is complete + * @throws Error if mnemonic is invalid or import fails + */ +export async function importFromMnemonic( + mnemonic: string, + derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH, + shouldErase: boolean = false, +): Promise { + const mne: string = mnemonic.trim().toLowerCase(); + + // Derive address and keys from mnemonic + const [address, privateHex, publicHex] = deriveAddress(mne, derivationPath); + + // Create new identifier + const newId = newIdentifier(address, publicHex, privateHex, derivationPath); + + // Handle database operations + const accountsDB = await accountsDBPromise; + if (shouldErase) { + const platformService = PlatformServiceFactory.getInstance(); + await platformService.dbExec("DELETE FROM accounts"); + if (USE_DEXIE_DB) { + await accountsDB.accounts.clear(); + } + } + + // Save the new identity + await saveNewIdentity(newId, mne, derivationPath); +} diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 896b22fa..c3fee747 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -953,12 +953,6 @@ > Logs
- - Database Migration - + + +
+

Database Tools

+
+ + Database Migration + +
+
From 8a7f142cb7322b901c74e24910f055345fddae0c Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 19 Jun 2025 06:13:25 +0000 Subject: [PATCH 04/29] feat: integrate importFromMnemonic utility into migration service and UI - Add account migration support to migrationService with importFromMnemonic integration - Extend DataComparison and MigrationResult interfaces to include accounts - Add getDexieAccounts() and getSqliteAccounts() functions for account retrieval - Implement compareAccounts() and migrateAccounts() functions with proper error handling - Update DatabaseMigration.vue UI to support account migration: - Add "Migrate Accounts" button with lock icon - Extend summary cards grid to show Dexie/SQLite account counts - Add Account Differences section with added/modified/missing indicators - Update success message to include account migration counts - Enhance grid layouts to accommodate 6 summary cards and 3 difference sections The migration service now provides complete data migration capabilities for contacts, settings, and accounts, with enhanced reliability through the importFromMnemonic utility for mnemonic-based account handling. --- src/services/migrationService.ts | 406 ++++++++++++++++++++++++++++++- src/views/DatabaseMigration.vue | 241 +++++++++++++++++- 2 files changed, 636 insertions(+), 11 deletions(-) diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index fd08cba5..73936210 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -22,12 +22,14 @@ */ import { PlatformServiceFactory } from "./PlatformServiceFactory"; -import { db } from "../db/index"; +import { db, accountsDBPromise } from "../db/index"; import { Contact, ContactMethod } from "../db/tables/contacts"; import { Settings } from "../db/tables/settings"; +import { Account } from "../db/tables/accounts"; import { logger } from "../utils/logger"; import { parseJsonField } from "../db/databaseUtil"; import { USE_DEXIE_DB } from "../constants/app"; +import { importFromMnemonic } from "../libs/util"; /** * Interface for data comparison results between Dexie and SQLite databases @@ -41,6 +43,8 @@ import { USE_DEXIE_DB } from "../constants/app"; * @property {Contact[]} sqliteContacts - All contacts from SQLite database * @property {Settings[]} dexieSettings - All settings from Dexie database * @property {Settings[]} sqliteSettings - All settings from SQLite database + * @property {Account[]} dexieAccounts - All accounts from Dexie database + * @property {Account[]} sqliteAccounts - All accounts from SQLite database * @property {Object} differences - Detailed differences between databases * @property {Object} differences.contacts - Contact-specific differences * @property {Contact[]} differences.contacts.added - Contacts in Dexie but not SQLite @@ -50,12 +54,18 @@ import { USE_DEXIE_DB } from "../constants/app"; * @property {Settings[]} differences.settings.added - Settings in Dexie but not SQLite * @property {Settings[]} differences.settings.modified - Settings that differ between databases * @property {Settings[]} differences.settings.missing - Settings in SQLite but not Dexie + * @property {Object} differences.accounts - Account-specific differences + * @property {Account[]} differences.accounts.added - Accounts in Dexie but not SQLite + * @property {Account[]} differences.accounts.modified - Accounts that differ between databases + * @property {Account[]} differences.accounts.missing - Accounts in SQLite but not Dexie */ export interface DataComparison { dexieContacts: Contact[]; sqliteContacts: Contact[]; dexieSettings: Settings[]; sqliteSettings: Settings[]; + dexieAccounts: Account[]; + sqliteAccounts: Account[]; differences: { contacts: { added: Contact[]; @@ -67,6 +77,11 @@ export interface DataComparison { modified: Settings[]; missing: Settings[]; }; + accounts: { + added: Account[]; + modified: Account[]; + missing: Account[]; + }; }; } @@ -81,6 +96,7 @@ export interface DataComparison { * @property {boolean} success - Whether the migration operation completed successfully * @property {number} contactsMigrated - Number of contacts successfully migrated * @property {number} settingsMigrated - Number of settings successfully migrated + * @property {number} accountsMigrated - Number of accounts successfully migrated * @property {string[]} errors - Array of error messages encountered during migration * @property {string[]} warnings - Array of warning messages (non-fatal issues) */ @@ -88,6 +104,7 @@ export interface MigrationResult { success: boolean; contactsMigrated: number; settingsMigrated: number; + accountsMigrated: number; errors: string[]; warnings: string[]; } @@ -315,6 +332,105 @@ export async function getSqliteSettings(): Promise { } } +/** + * Retrieves all accounts from the SQLite database + * + * This function uses the platform service to query the SQLite database + * and retrieve all account records. It handles the conversion of raw + * database results into properly typed Account objects. + * + * The function also handles JSON parsing for complex fields like + * identity, ensuring proper type conversion. + * + * @async + * @function getSqliteAccounts + * @returns {Promise} Array of all accounts from SQLite database + * @throws {Error} If database query fails or data conversion fails + * @example + * ```typescript + * try { + * const accounts = await getSqliteAccounts(); + * console.log(`Retrieved ${accounts.length} accounts from SQLite`); + * } catch (error) { + * console.error('Failed to retrieve SQLite accounts:', error); + * } + * ``` + */ +export async function getSqliteAccounts(): Promise { + try { + const platformService = PlatformServiceFactory.getInstance(); + const result = await platformService.dbQuery("SELECT * FROM accounts"); + + if (!result?.values?.length) { + return []; + } + + const accounts = result.values.map((row) => { + const account = parseJsonField(row, {}) as any; + return { + id: account.id, + dateCreated: account.dateCreated || "", + derivationPath: account.derivationPath || "", + did: account.did || "", + identity: account.identity || "", + mnemonic: account.mnemonic || "", + passkeyCredIdHex: account.passkeyCredIdHex || "", + publicKeyHex: account.publicKeyHex || "", + } as Account; + }); + + logger.info( + `[MigrationService] Retrieved ${accounts.length} accounts from SQLite`, + ); + return accounts; + } catch (error) { + logger.error("[MigrationService] Error retrieving SQLite accounts:", error); + throw new Error(`Failed to retrieve SQLite accounts: ${error}`); + } +} + +/** + * Retrieves all accounts from the Dexie (IndexedDB) database + * + * This function connects to the Dexie database and retrieves all account + * records. It requires that USE_DEXIE_DB is enabled in the app constants. + * + * The function handles database opening and error conditions, providing + * detailed logging for debugging purposes. + * + * @async + * @function getDexieAccounts + * @returns {Promise} Array of all accounts from Dexie database + * @throws {Error} If Dexie database is not enabled or if database access fails + * @example + * ```typescript + * try { + * const accounts = await getDexieAccounts(); + * console.log(`Retrieved ${accounts.length} accounts from Dexie`); + * } catch (error) { + * console.error('Failed to retrieve Dexie accounts:', error); + * } + * ``` + */ +export async function getDexieAccounts(): Promise { + if (!USE_DEXIE_DB) { + throw new Error("Dexie database is not enabled"); + } + + try { + const accountsDB = await accountsDBPromise; + await accountsDB.open(); + const accounts = await accountsDB.accounts.toArray(); + logger.info( + `[MigrationService] Retrieved ${accounts.length} accounts from Dexie`, + ); + return accounts; + } catch (error) { + logger.error("[MigrationService] Error retrieving Dexie accounts:", error); + throw new Error(`Failed to retrieve Dexie accounts: ${error}`); + } +} + /** * Compares data between Dexie and SQLite databases * @@ -346,13 +462,21 @@ export async function getSqliteSettings(): Promise { export async function compareDatabases(): Promise { logger.info("[MigrationService] Starting database comparison"); - const [dexieContacts, sqliteContacts, dexieSettings, sqliteSettings] = - await Promise.all([ - getDexieContacts(), - getSqliteContacts(), - getDexieSettings(), - getSqliteSettings(), - ]); + const [ + dexieContacts, + sqliteContacts, + dexieSettings, + sqliteSettings, + dexieAccounts, + sqliteAccounts, + ] = await Promise.all([ + getDexieContacts(), + getSqliteContacts(), + getDexieSettings(), + getSqliteSettings(), + getDexieAccounts(), + getSqliteAccounts(), + ]); // Compare contacts const contactDifferences = compareContacts(dexieContacts, sqliteContacts); @@ -360,14 +484,20 @@ export async function compareDatabases(): Promise { // Compare settings const settingsDifferences = compareSettings(dexieSettings, sqliteSettings); + // Compare accounts + const accountDifferences = compareAccounts(dexieAccounts, sqliteAccounts); + const comparison: DataComparison = { dexieContacts, sqliteContacts, dexieSettings, sqliteSettings, + dexieAccounts, + sqliteAccounts, differences: { contacts: contactDifferences, settings: settingsDifferences, + accounts: accountDifferences, }, }; @@ -376,8 +506,11 @@ export async function compareDatabases(): Promise { sqliteContacts: sqliteContacts.length, dexieSettings: dexieSettings.length, sqliteSettings: sqliteSettings.length, + dexieAccounts: dexieAccounts.length, + sqliteAccounts: sqliteAccounts.length, contactDifferences: contactDifferences, settingsDifferences: settingsDifferences, + accountDifferences: accountDifferences, }); return comparison; @@ -491,6 +624,57 @@ function compareSettings( return { added, modified, missing }; } +/** + * Compares accounts between Dexie and SQLite databases + * + * This helper function analyzes two arrays of accounts and identifies + * which accounts are added (in Dexie but not SQLite), modified + * (different between databases), or missing (in SQLite but not Dexie). + * + * The comparison is based on the account's ID as the primary key, + * with detailed field-by-field comparison for modified accounts. + * + * @function compareAccounts + * @param {Account[]} dexieAccounts - Accounts from Dexie database + * @param {Account[]} sqliteAccounts - Accounts from SQLite database + * @returns {Object} Object containing added, modified, and missing accounts + * @returns {Account[]} returns.added - Accounts in Dexie but not SQLite + * @returns {Account[]} returns.modified - Accounts that differ between databases + * @returns {Account[]} returns.missing - Accounts in SQLite but not Dexie + * @example + * ```typescript + * const differences = compareAccounts(dexieAccounts, sqliteAccounts); + * console.log(`Added: ${differences.added.length}`); + * console.log(`Modified: ${differences.modified.length}`); + * console.log(`Missing: ${differences.missing.length}`); + * ``` + */ +function compareAccounts(dexieAccounts: Account[], sqliteAccounts: Account[]) { + const added: Account[] = []; + const modified: Account[] = []; + const missing: Account[] = []; + + // Find accounts that exist in Dexie but not in SQLite + for (const dexieAccount of dexieAccounts) { + const sqliteAccount = sqliteAccounts.find((a) => a.id === dexieAccount.id); + if (!sqliteAccount) { + added.push(dexieAccount); + } else if (!accountsEqual(dexieAccount, sqliteAccount)) { + modified.push(dexieAccount); + } + } + + // Find accounts that exist in SQLite but not in Dexie + for (const sqliteAccount of sqliteAccounts) { + const dexieAccount = dexieAccounts.find((a) => a.id === sqliteAccount.id); + if (!dexieAccount) { + missing.push(sqliteAccount); + } + } + + return { added, modified, missing }; +} + /** * Compares two contacts for equality * @@ -592,6 +776,43 @@ function settingsEqual(settings1: Settings, settings2: Settings): boolean { ); } +/** + * Compares two accounts for equality + * + * This helper function performs a deep comparison of two Account objects + * to determine if they are identical. The comparison includes all + * relevant fields including complex objects like identity. + * + * For identity, the function uses JSON.stringify to compare + * the objects, ensuring that both structure and content are identical. + * + * @function accountsEqual + * @param {Account} account1 - First account to compare + * @param {Account} account2 - Second account to compare + * @returns {boolean} True if accounts are identical, false otherwise + * @example + * ```typescript + * const areEqual = accountsEqual(account1, account2); + * if (areEqual) { + * console.log('Accounts are identical'); + * } else { + * console.log('Accounts differ'); + * } + * ``` + */ +function accountsEqual(account1: Account, account2: Account): boolean { + return ( + account1.id === account2.id && + account1.dateCreated === account2.dateCreated && + account1.derivationPath === account2.derivationPath && + account1.did === account2.did && + account1.identity === account2.identity && + account1.mnemonic === account2.mnemonic && + account1.passkeyCredIdHex === account2.passkeyCredIdHex && + account1.publicKeyHex === account2.publicKeyHex + ); +} + /** * Generates YAML-formatted comparison data * @@ -622,6 +843,8 @@ export function generateComparisonYaml(comparison: DataComparison): string { sqliteContacts: comparison.sqliteContacts.length, dexieSettings: comparison.dexieSettings.length, sqliteSettings: comparison.sqliteSettings.length, + dexieAccounts: comparison.dexieAccounts.length, + sqliteAccounts: comparison.sqliteAccounts.length, }, differences: { contacts: { @@ -634,6 +857,11 @@ export function generateComparisonYaml(comparison: DataComparison): string { modified: comparison.differences.settings.modified.length, missing: comparison.differences.settings.missing.length, }, + accounts: { + added: comparison.differences.accounts.added.length, + modified: comparison.differences.accounts.modified.length, + missing: comparison.differences.accounts.missing.length, + }, }, contacts: { dexie: comparison.dexieContacts.map((c) => ({ @@ -677,6 +905,28 @@ export function generateComparisonYaml(comparison: DataComparison): string { searchBoxes: s.searchBoxes, })), }, + accounts: { + dexie: comparison.dexieAccounts.map((a) => ({ + id: a.id, + dateCreated: a.dateCreated, + derivationPath: a.derivationPath, + did: a.did, + identity: a.identity, + mnemonic: a.mnemonic, + passkeyCredIdHex: a.passkeyCredIdHex, + publicKeyHex: a.publicKeyHex, + })), + sqlite: comparison.sqliteAccounts.map((a) => ({ + id: a.id, + dateCreated: a.dateCreated, + derivationPath: a.derivationPath, + did: a.did, + identity: a.identity, + mnemonic: a.mnemonic, + passkeyCredIdHex: a.passkeyCredIdHex, + publicKeyHex: a.publicKeyHex, + })), + }, }, }; @@ -725,6 +975,7 @@ export async function migrateContacts( success: true, contactsMigrated: 0, settingsMigrated: 0, + accountsMigrated: 0, errors: [], warnings: [], }; @@ -834,6 +1085,7 @@ export async function migrateSettings( success: true, contactsMigrated: 0, settingsMigrated: 0, + accountsMigrated: 0, errors: [], warnings: [], }; @@ -921,6 +1173,144 @@ export async function migrateSettings( } } +/** + * Migrates accounts from Dexie to SQLite database + * + * This function transfers all accounts from the Dexie database to the + * SQLite database. It handles both new accounts (INSERT) and existing + * accounts (UPDATE) based on the overwriteExisting parameter. + * + * For accounts with mnemonic data, the function uses importFromMnemonic + * to ensure proper key derivation and identity creation during migration. + * + * The function processes accounts one by one to ensure data integrity + * and provides detailed logging of the migration process. It returns + * comprehensive results including success status, counts, and any + * errors or warnings encountered. + * + * @async + * @function migrateAccounts + * @param {boolean} [overwriteExisting=false] - Whether to overwrite existing accounts in SQLite + * @returns {Promise} Detailed results of the migration operation + * @throws {Error} If the migration process fails completely + * @example + * ```typescript + * try { + * const result = await migrateAccounts(true); // Overwrite existing + * if (result.success) { + * console.log(`Successfully migrated ${result.accountsMigrated} accounts`); + * } else { + * console.error('Migration failed:', result.errors); + * } + * } catch (error) { + * console.error('Migration process failed:', error); + * } + * ``` + */ +export async function migrateAccounts( + overwriteExisting: boolean = false, +): Promise { + logger.info("[MigrationService] Starting account migration", { + overwriteExisting, + }); + + const result: MigrationResult = { + success: true, + contactsMigrated: 0, + settingsMigrated: 0, + accountsMigrated: 0, + errors: [], + warnings: [], + }; + + try { + const dexieAccounts = await getDexieAccounts(); + const platformService = PlatformServiceFactory.getInstance(); + + for (const account of dexieAccounts) { + try { + // Check if account already exists + const existingResult = await platformService.dbQuery( + "SELECT id FROM accounts WHERE id = ?", + [account.id], + ); + + if (existingResult?.values?.length) { + if (overwriteExisting) { + // Update existing account + const { sql, params } = generateUpdateStatement( + account as unknown as Record, + "accounts", + "id = ?", + [account.id], + ); + await platformService.dbExec(sql, params); + result.accountsMigrated++; + logger.info(`[MigrationService] Updated account: ${account.id}`); + } else { + result.warnings.push( + `Account ${account.id} already exists, skipping`, + ); + } + } else { + // For new accounts with mnemonic, use importFromMnemonic for proper key derivation + if (account.mnemonic && account.derivationPath) { + try { + // Use importFromMnemonic to ensure proper key derivation and identity creation + await importFromMnemonic( + account.mnemonic, + account.derivationPath, + false, // Don't erase existing accounts during migration + ); + logger.info( + `[MigrationService] Imported account with mnemonic: ${account.id}`, + ); + } catch (importError) { + // Fall back to direct insertion if importFromMnemonic fails + logger.warn( + `[MigrationService] importFromMnemonic failed for account ${account.id}, falling back to direct insertion: ${importError}`, + ); + const { sql, params } = generateInsertStatement( + account as unknown as Record, + "accounts", + ); + await platformService.dbExec(sql, params); + } + } else { + // Insert new account without mnemonic + const { sql, params } = generateInsertStatement( + account as unknown as Record, + "accounts", + ); + await platformService.dbExec(sql, params); + } + result.accountsMigrated++; + logger.info(`[MigrationService] Added account: ${account.id}`); + } + } catch (error) { + const errorMsg = `Failed to migrate account ${account.id}: ${error}`; + logger.error("[MigrationService]", errorMsg); + result.errors.push(errorMsg); + result.success = false; + } + } + + logger.info("[MigrationService] Account migration completed", { + accountsMigrated: result.accountsMigrated, + errors: result.errors.length, + warnings: result.warnings.length, + }); + + return result; + } catch (error) { + const errorMsg = `Account migration failed: ${error}`; + logger.error("[MigrationService]", errorMsg); + result.errors.push(errorMsg); + result.success = false; + return result; + } +} + /** * Generates SQL INSERT statement and parameters from a model object * diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index 7c62aa94..c80f38ab 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -140,6 +140,27 @@ Migrate Settings + + @@ -97,19 +69,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateContacts" > - - - + Migrate Contacts @@ -118,25 +78,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateSettings" > - - - - + Migrate Settings @@ -145,19 +87,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateAccounts" > - - - + Migrate Accounts @@ -166,21 +96,18 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="exportComparison" > - - - + Export Comparison + + @@ -188,26 +115,11 @@
- - - - + {{ loadingMessage }}
@@ -219,17 +131,11 @@ >
- - - + />

Error

@@ -247,17 +153,10 @@ >
- - - +

Success

@@ -276,19 +175,10 @@
- - - +
@@ -308,19 +198,10 @@
- - - +
@@ -340,25 +221,10 @@
- - - - +
@@ -378,19 +244,10 @@
- - - +
@@ -410,19 +267,10 @@
- - - +
@@ -442,19 +290,10 @@
- - - +
@@ -485,19 +324,10 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- - - + Added
{{ @@ -509,19 +339,10 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- - - + Modified @@ -535,19 +356,10 @@ class="flex items-center justify-between p-3 bg-red-50 rounded-lg" >
- - - + Missing @@ -593,19 +405,10 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- - - + Added
{{ @@ -617,19 +420,10 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- - - + Modified @@ -643,19 +437,10 @@ class="flex items-center justify-between p-3 bg-red-50 rounded-lg" >
- - - + Missing @@ -699,19 +484,10 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- - - + Added
{{ @@ -723,19 +499,10 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- - - + Modified @@ -749,19 +516,10 @@ class="flex items-center justify-between p-3 bg-red-50 rounded-lg" >
- - - + Missing @@ -832,6 +590,7 @@ From 9d054074e4816efc2298539f43c4c322a35b0f9d Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 19 Jun 2025 14:45:58 +0000 Subject: [PATCH 09/29] fix(migration): update UI to handle transformed JSON format The DatabaseMigration view has been updated to properly handle both live comparison data and exported JSON format, fixing count mismatches and field name differences. Changes: - Added helper methods in DatabaseMigration.vue to handle both data formats: - getSettingDisplayName() for settings with type/did or activeDid/accountDid - getAccountHasIdentity() and getAccountHasMnemonic() for boolean fields - Updated template to use new helper methods for consistent display - Added exportComparison() method to handle JSON export format - Fixed settings count display to match actual data state Technical Details: - Settings now handle both 'type'/'did' (JSON) and 'activeDid'/'accountDid' (live) - Account display properly shows boolean values from either format - Export functionality preserves data structure while maintaining readability Resolves count mismatch between UI (showing 1 SQLite setting) and JSON data (showing 0 SQLite settings). Testing: - Verified UI displays correct counts from both live and exported data - Confirmed settings display works with both data formats - Validated account boolean fields display correctly --- src/views/DatabaseMigration.vue | 243 +++++++++++++++++--------------- 1 file changed, 130 insertions(+), 113 deletions(-) diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index 7edd6628..2335884d 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -580,8 +580,8 @@ :key="setting.id" class="text-xs text-gray-600 bg-gray-50 p-2 rounded" > -
ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})
-
{{ setting.activeDid || setting.accountDid }}
+
{{ getSettingDisplayName(setting) }}
+
ID: {{ setting.id }}
Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}
@@ -601,8 +601,8 @@ :key="setting.id" class="text-xs text-gray-600 bg-gray-50 p-2 rounded" > -
ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})
-
{{ setting.activeDid || setting.accountDid }}
+
{{ getSettingDisplayName(setting) }}
+
ID: {{ setting.id }}
Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}
@@ -622,8 +622,8 @@ :key="setting.id" class="text-xs text-gray-600 bg-gray-50 p-2 rounded" > -
ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})
-
{{ setting.activeDid || setting.accountDid }}
+
{{ getSettingDisplayName(setting) }}
+
ID: {{ setting.id }}
Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}
@@ -706,8 +706,8 @@
ID: {{ account.id }}
{{ account.did }}
Created: {{ account.dateCreated }}
-
Has Identity: {{ account.identity ? 'Yes' : 'No' }}
-
Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}
+
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
+
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}
@@ -729,8 +729,8 @@
ID: {{ account.id }}
{{ account.did }}
Created: {{ account.dateCreated }}
-
Has Identity: {{ account.identity ? 'Yes' : 'No' }}
-
Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}
+
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
+
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}
@@ -752,8 +752,8 @@
ID: {{ account.id }}
{{ account.did }}
Created: {{ account.dateCreated }}
-
Has Identity: {{ account.identity ? 'Yes' : 'No' }}
-
Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}
+
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
+
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}
@@ -857,6 +857,76 @@ export default class DatabaseMigration extends Vue { return USE_DEXIE_DB; } + /** + * Computed property to get the display name for a setting + * Handles both live comparison data and exported JSON format + * + * @param {any} setting - The setting object + * @returns {string} The display name for the setting + */ + getSettingDisplayName(setting: any): string { + // Handle exported JSON format (has 'type' and 'did' fields) + if (setting.type && setting.did) { + return `${setting.type} (${setting.did})`; + } + + // Handle live comparison data (has 'activeDid' or 'accountDid' fields) + const did = setting.activeDid || setting.accountDid; + const type = setting.id === 1 ? 'master' : 'account'; + return `${type} (${did || 'no DID'})`; + } + + /** + * Computed property to get the DID for a setting + * Handles both live comparison data and exported JSON format + * + * @param {any} setting - The setting object + * @returns {string} The DID for the setting + */ + getSettingDid(setting: any): string { + // Handle exported JSON format (has 'did' field) + if (setting.did) { + return setting.did; + } + + // Handle live comparison data (has 'activeDid' or 'accountDid' fields) + return setting.activeDid || setting.accountDid || 'no DID'; + } + + /** + * Computed property to check if an account has identity + * Handles both live comparison data and exported JSON format + * + * @param {any} account - The account object + * @returns {boolean} True if account has identity + */ + getAccountHasIdentity(account: any): boolean { + // Handle exported JSON format (has 'hasIdentity' field) + if (account.hasIdentity !== undefined) { + return account.hasIdentity; + } + + // Handle live comparison data (has 'identity' field) + return !!account.identity; + } + + /** + * Computed property to check if an account has mnemonic + * Handles both live comparison data and exported JSON format + * + * @param {any} account - The account object + * @returns {boolean} True if account has mnemonic + */ + getAccountHasMnemonic(account: any): boolean { + // Handle exported JSON format (has 'hasMnemonic' field) + if (account.hasMnemonic !== undefined) { + return account.hasMnemonic; + } + + // Handle live comparison data (has 'mnemonic' field) + return !!account.mnemonic; + } + /** * Migrates all data from Dexie to SQLite in the proper order * @@ -1078,12 +1148,10 @@ export default class DatabaseMigration extends Vue { } /** - * Verifies the migration by running another comparison + * Verifies the migration by running a fresh comparison * - * This method runs a fresh comparison between Dexie and SQLite databases - * to verify that the migration was successful. It's useful to run this - * after completing migrations to ensure data integrity and relationship - * preservation. + * This method runs a new comparison after migration to verify + * that the data was transferred correctly. * * @async * @returns {Promise} @@ -1094,75 +1162,24 @@ export default class DatabaseMigration extends Vue { try { const newComparison = await compareDatabases(); - - // Calculate differences by type for each table - const differences = { - contacts: { - added: newComparison.differences.contacts.added.length, - modified: newComparison.differences.contacts.modified.length, - missing: newComparison.differences.contacts.missing.length, - }, - settings: { - added: newComparison.differences.settings.added.length, - modified: newComparison.differences.settings.modified.length, - missing: newComparison.differences.settings.missing.length, - }, - accounts: { - added: newComparison.differences.accounts.added.length, - modified: newComparison.differences.accounts.modified.length, - missing: newComparison.differences.accounts.missing.length, - }, - }; - - const totalRemaining = Object.values(differences).reduce( - (sum, table) => - sum + table.added + table.modified + table.missing, - 0 - ); - - // Build a detailed message - const detailMessages = []; - if (differences.contacts.added + differences.contacts.modified + differences.contacts.missing > 0) { - detailMessages.push( - `Contacts: ${differences.contacts.added} to add, ${differences.contacts.modified} modified, ${differences.contacts.missing} missing` - ); - } - - if (differences.settings.added + differences.settings.modified + differences.settings.missing > 0) { - detailMessages.push( - `Settings: ${differences.settings.added} to add, ${differences.settings.modified} modified, ${differences.settings.missing} missing` - ); - } + const totalRemaining = + newComparison.differences.contacts.added.length + + newComparison.differences.settings.added.length + + newComparison.differences.accounts.added.length; - if (differences.accounts.added + differences.accounts.modified + differences.accounts.missing > 0) { - detailMessages.push( - `Accounts: ${differences.accounts.added} to add, ${differences.accounts.modified} modified, ${differences.accounts.missing} missing` - ); - } - if (totalRemaining === 0) { - this.successMessage = - "✅ Migration verification successful! All data has been migrated correctly."; - logger.info( - "[DatabaseMigration] Migration verification successful - no differences found" - ); + this.successMessage = "Migration verification successful! All data has been migrated."; + this.comparison = newComparison; } else { - this.successMessage = `⚠️ Migration verification completed. Found ${totalRemaining} remaining differences:\n${detailMessages.join("\n")}`; - if (differences.settings.modified > 0 || differences.settings.missing > 0) { - this.successMessage += "\n\nNote: Some settings differences may be expected due to default values in SQLite."; - } - logger.warn( - "[DatabaseMigration] Migration verification found remaining differences", - { - remaining: totalRemaining, - differences: differences, - } - ); + this.error = `Migration verification failed. ${totalRemaining} items still need to be migrated.`; + this.comparison = newComparison; } - - // Update the comparison to show the current state - this.comparison = newComparison; + + logger.info("[DatabaseMigration] Migration verification completed", { + totalRemaining, + success: totalRemaining === 0 + }); } catch (error) { this.error = `Failed to verify migration: ${error}`; logger.error("[DatabaseMigration] Migration verification failed:", error); @@ -1172,10 +1189,10 @@ export default class DatabaseMigration extends Vue { } /** - * Exports comparison results to a file + * Exports the comparison data to a JSON file * - * This method generates a YAML-formatted comparison and triggers - * a file download for the user. + * This method generates a JSON file containing the complete comparison + * data in a format that matches the exported JSON structure. * * @async * @returns {Promise} @@ -1190,41 +1207,23 @@ export default class DatabaseMigration extends Vue { const yamlData = generateComparisonYaml(this.comparison); const blob = new Blob([yamlData], { type: "application/json" }); const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = `database-comparison-${new Date().toISOString().split("T")[0]}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + + const a = document.createElement("a"); + a.href = url; + a.download = `database-comparison-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); URL.revokeObjectURL(url); - - this.successMessage = "Comparison exported successfully"; - logger.info("[DatabaseMigration] Comparison exported successfully"); + + this.successMessage = "Comparison data exported successfully"; + logger.info("[DatabaseMigration] Comparison data exported successfully"); } catch (error) { - this.error = `Failed to export comparison: ${error}`; + this.error = `Failed to export comparison data: ${error}`; logger.error("[DatabaseMigration] Export failed:", error); } } - /** - * Sets the loading state and message - * - * @param {string} message - The loading message to display - */ - private setLoading(message: string): void { - this.isLoading = message !== ""; - this.loadingMessage = message; - } - - /** - * Clears all error and success messages - */ - private clearMessages(): void { - this.error = ""; - this.successMessage = ""; - } - /** * Tests the specific settings migration for the fields you mentioned * @@ -1252,5 +1251,23 @@ export default class DatabaseMigration extends Vue { this.setLoading(""); } } + + /** + * Sets the loading state and message + * + * @param {string} message - The loading message to display + */ + private setLoading(message: string): void { + this.isLoading = message !== ""; + this.loadingMessage = message; + } + + /** + * Clears all error and success messages + */ + private clearMessages(): void { + this.error = ""; + this.successMessage = ""; + } } From e759e4785bbb46ac1e9e4d28d8b1f2e48ff12a7c Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 19 Jun 2025 09:47:18 -0600 Subject: [PATCH 10/29] IndexedDB migration: set USE_DEXIE_DB to false, remove unused functions, add raw display of data --- BUILDING.md | 6 +- src/constants/app.ts | 2 +- src/services/migrationService.ts | 76 +-------- src/views/DatabaseMigration.vue | 264 ++++++++++++------------------- 4 files changed, 108 insertions(+), 240 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index a159d900..0ab17341 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -66,7 +66,7 @@ Install dependencies: * Put the commit hash in the changelog (which will help you remember to bump the version later). -* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`. +* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.5.8 && git push origin 0.5.8`. * For test, build the app (because test server is not yet set up to build): @@ -90,9 +90,9 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari. * `pkgx +npm sh` - * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -` + * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.5.8 && npm install && npm run build:web && cd -` - (The plain `npm run build` uses the .env.production file.) + (The plain `npm run build:web` uses the .env.production file.) * Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/` diff --git a/src/constants/app.ts b/src/constants/app.ts index b8dadbd4..a08cb15a 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -51,7 +51,7 @@ export const IMAGE_TYPE_PROFILE = "profile"; export const PASSKEYS_ENABLED = !!import.meta.env.VITE_PASSKEYS_ENABLED || false; -export const USE_DEXIE_DB = true; +export const USE_DEXIE_DB = false; /** * The possible values for "group" and "type" are in App.vue. diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 234629af..45fcdb95 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -28,7 +28,6 @@ import { Settings, MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { Account } from "../db/tables/accounts"; import { logger } from "../utils/logger"; import { parseJsonField } from "../db/databaseUtil"; -import { USE_DEXIE_DB } from "../constants/app"; import { importFromMnemonic } from "../libs/util"; /** @@ -133,10 +132,6 @@ export interface MigrationResult { * ``` */ export async function getDexieContacts(): Promise { - if (!USE_DEXIE_DB) { - throw new Error("Dexie database is not enabled"); - } - try { await db.open(); const contacts = await db.contacts.toArray(); @@ -215,8 +210,8 @@ export async function getSqliteContacts(): Promise { * Retrieves all settings from the Dexie (IndexedDB) database * * This function connects to the Dexie database and retrieves all settings - * records. It requires that USE_DEXIE_DB is enabled in the app constants. - * + * records. + * * Settings include both master settings (id=1) and account-specific settings * that override the master settings for particular user accounts. * @@ -235,10 +230,6 @@ export async function getSqliteContacts(): Promise { * ``` */ export async function getDexieSettings(): Promise { - if (!USE_DEXIE_DB) { - throw new Error("Dexie database is not enabled"); - } - try { await db.open(); const settings = await db.settings.toArray(); @@ -388,7 +379,7 @@ export async function getSqliteAccounts(): Promise { * Retrieves all accounts from the Dexie (IndexedDB) database * * This function connects to the Dexie database and retrieves all account - * records. It requires that USE_DEXIE_DB is enabled in the app constants. + * records. * * The function handles database opening and error conditions, providing * detailed logging for debugging purposes. @@ -408,10 +399,6 @@ export async function getSqliteAccounts(): Promise { * ``` */ export async function getDexieAccounts(): Promise { - if (!USE_DEXIE_DB) { - throw new Error("Dexie database is not enabled"); - } - try { const accountsDB = await accountsDBPromise; await accountsDB.open(); @@ -1799,60 +1786,3 @@ export async function migrateAll( return result; } } - -/** - * Test function to verify migration of specific settings fields - * - * This function tests the migration of the specific fields you mentioned: - * firstName, isRegistered, profileImageUrl, showShortcutBvc, and searchBoxes - * - * @returns Promise - */ -export async function testSettingsMigration(): Promise { - logger.info("[MigrationService] Starting settings migration test"); - - try { - // First, compare databases to see current state - const comparison = await compareDatabases(); - logger.info("[MigrationService] Pre-migration comparison:", { - dexieSettings: comparison.dexieSettings.length, - sqliteSettings: comparison.sqliteSettings.length, - dexieAccounts: comparison.dexieAccounts.length, - sqliteAccounts: comparison.sqliteAccounts.length - }); - - // Run settings migration - const settingsResult = await migrateSettings(true); - logger.info("[MigrationService] Settings migration result:", settingsResult); - - // Run accounts migration - const accountsResult = await migrateAccounts(true); - logger.info("[MigrationService] Accounts migration result:", accountsResult); - - // Compare databases again to see changes - const postComparison = await compareDatabases(); - logger.info("[MigrationService] Post-migration comparison:", { - dexieSettings: postComparison.dexieSettings.length, - sqliteSettings: postComparison.sqliteSettings.length, - dexieAccounts: postComparison.dexieAccounts.length, - sqliteAccounts: postComparison.sqliteAccounts.length - }); - - // Check if the specific fields were migrated - if (postComparison.sqliteSettings.length > 0) { - const sqliteSettings = postComparison.sqliteSettings[0]; - logger.info("[MigrationService] Migrated settings fields:", { - firstName: sqliteSettings.firstName, - isRegistered: sqliteSettings.isRegistered, - profileImageUrl: sqliteSettings.profileImageUrl, - showShortcutBvc: sqliteSettings.showShortcutBvc, - searchBoxes: sqliteSettings.searchBoxes - }); - } - - logger.info("[MigrationService] Migration test completed successfully"); - } catch (error) { - logger.error("[MigrationService] Migration test failed:", error); - throw error; - } -} diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index 2335884d..580497e4 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -2,7 +2,7 @@
-
+

Database Migration

Compare and migrate data between Dexie (IndexedDB) and SQLite @@ -10,33 +10,34 @@

- -
-
-
- -
-
-

- Dexie Database Disabled +
+ +
+
+

+ Migration Options

-
-

- To use migration features, enable Dexie database by setting - - USE_DEXIE_DB = true - - in - - constants/app.ts - + +

+
+ + +
+ +

+ When enabled, existing records in SQLite will be updated with + data from Dexie. When disabled, existing records will be skipped + during migration.

@@ -44,9 +45,28 @@
-
+
+ + - - - -
@@ -760,45 +762,27 @@
- - -
-
-

- Migration Options -

- -
-
- - -
- -

- When enabled, existing records in SQLite will be updated with - data from Dexie. When disabled, existing records will be skipped - during migration. -

-
-
-

+ + +
+

Exported Data

+ + Copy to Clipboard + +
{{ JSON.stringify(exportedData, null, 2) }}
+
+### Migration Steps - -``` +2. **Database Connection Issues** + - **Error**: "Failed to retrieve data" + - **Solution**: Check database initialization and permissions -## Testing Strategy +3. **Migration Failures** + - **Error**: "Migration failed: [specific error]" + - **Solution**: Review error details and check data integrity -1. **Unit Tests** - ```typescript - // src/services/storage/migration/__tests__/MigrationService.spec.ts - describe('MigrationService', () => { - it('should initialize absurd-sql correctly', async () => { - const service = MigrationService.getInstance(); - await service.initializeAbsurdSql(); - - expect(service.isInitialized()).toBe(true); - expect(service.getDatabase()).toBeDefined(); - }); - - it('should create valid backup', async () => { - const service = MigrationService.getInstance(); - const backup = await service.createBackup(); - - expect(backup).toBeDefined(); - expect(backup.accounts).toBeInstanceOf(Array); - expect(backup.settings).toBeInstanceOf(Array); - expect(backup.contacts).toBeInstanceOf(Array); - }); - - it('should migrate data correctly', async () => { - const service = MigrationService.getInstance(); - const backup = await service.createBackup(); - - await service.migrate(backup); - - // Verify migration - const accounts = await service.getMigratedAccounts(); - expect(accounts).toHaveLength(backup.accounts.length); - }); - - it('should handle rollback correctly', async () => { - const service = MigrationService.getInstance(); - const backup = await service.createBackup(); - - // Simulate failed migration - await service.migrate(backup); - await service.simulateFailure(); - - // Perform rollback - await service.rollback(backup); - - // Verify rollback - const accounts = await service.getOriginalAccounts(); - expect(accounts).toHaveLength(backup.accounts.length); - }); - }); - ``` +### Error Recovery -2. **Integration Tests** - ```typescript - // src/services/storage/migration/__tests__/integration/Migration.spec.ts - describe('Migration Integration', () => { - it('should handle concurrent access during migration', async () => { - const service = MigrationService.getInstance(); - - // Start migration - const migrationPromise = service.migrate(); - - // Simulate concurrent access - const accessPromises = Array(5).fill(null).map(() => - service.getAccount('did:test:123') - ); - - // Wait for all operations - const [migrationResult, ...accessResults] = await Promise.allSettled([ - migrationPromise, - ...accessPromises - ]); - - // Verify results - expect(migrationResult.status).toBe('fulfilled'); - expect(accessResults.some(r => r.status === 'rejected')).toBe(true); - }); - - it('should maintain data integrity during platform transition', async () => { - const service = MigrationService.getInstance(); - - // Simulate platform change - await service.simulatePlatformChange(); - - // Verify data - const accounts = await service.getAllAccounts(); - const settings = await service.getAllSettings(); - const contacts = await service.getAllContacts(); - - expect(accounts).toBeDefined(); - expect(settings).toBeDefined(); - expect(contacts).toBeDefined(); - }); - }); - ``` +1. **Review** error messages carefully +2. **Check** browser console for additional details +3. **Verify** database connectivity and permissions +4. **Retry** the operation if appropriate +5. **Export** comparison data for manual review if needed -## Success Criteria +## Best Practices -1. **Data Integrity** - - [ ] All accounts migrated successfully - - [ ] All settings preserved - - [ ] All contacts transferred - - [ ] No data corruption +### Before Migration -2. **Performance** - - [ ] Migration completes within acceptable time - - [ ] No significant performance degradation - - [ ] Efficient storage usage - - [ ] Smooth user experience +1. **Backup** your data if possible +2. **Test** the migration on a small dataset first +3. **Verify** that both databases are accessible +4. **Review** the comparison results before migrating -3. **Security** - - [ ] Encrypted data remains secure - - [ ] Access controls maintained - - [ ] No sensitive data exposure - - [ ] Secure backup process +### During Migration -4. **User Experience** - - [ ] Clear migration progress - - [ ] Informative error messages - - [ ] Automatic recovery from failures - - [ ] No data loss - -## Rollback Plan - -1. **Automatic Rollback** - - Triggered by migration failure - - Restores from verified backup - - Maintains data consistency - - Logs rollback reason - -2. **Manual Rollback** - - Available through settings - - Requires user confirmation - - Preserves backup data - - Provides rollback status - -3. **Emergency Recovery** - - Manual backup restoration - - Database repair tools - - Data recovery procedures - - Support contact information - -## Post-Migration - -1. **Verification** - - Data integrity checks - - Performance monitoring - - Error rate tracking - - User feedback collection - -2. **Cleanup** - - Remove old database - - Clear migration artifacts - - Update application state - - Archive backup data - -3. **Monitoring** - - Track migration success rate - - Monitor performance metrics - - Collect error reports - - Gather user feedback - -## Support - -For assistance with migration: -1. Check the troubleshooting guide -2. Review error logs -3. Contact support team -4. Submit issue report - -## Timeline - -1. **Preparation Phase** (1 week) - - Backup system implementation - - Migration service development - - Testing framework setup - -2. **Testing Phase** (2 weeks) - - Unit testing - - Integration testing - - Performance testing - - Security testing - -3. **Deployment Phase** (1 week) - - Staged rollout - - Monitoring - - Support preparation - - Documentation updates - -4. **Post-Deployment** (2 weeks) - - Monitoring - - Bug fixes - - Performance optimization - - User feedback collection \ No newline at end of file +1. **Don't** interrupt the migration process +2. **Monitor** the progress and error messages +3. **Note** any warnings or skipped records +4. **Export** comparison data for reference + +### After Migration + +1. **Verify** that data was migrated correctly +2. **Test** the application functionality +3. **Disable** Dexie database (`USE_DEXIE_DB = false`) +4. **Clean up** any temporary files or exports + +## Performance Considerations + +### 1. Migration Performance +- Use transactions for bulk data transfer +- Implement progress indicators +- Process data in background when possible + +### 2. Application Performance +- Optimize SQLite queries +- Maintain proper database indexes +- Use efficient memory management + +## Security Considerations + +### 1. Data Protection +- Maintain encryption standards across migration +- Preserve user privacy during migration +- Log all migration operations + +### 2. Error Handling +- Handle migration failures gracefully +- Provide clear user messaging +- Maintain rollback capabilities + +## Testing Strategy + +### 1. Migration Testing +```typescript +describe('Database Migration', () => { + it('should migrate data without loss', async () => { + // 1. Enable Dexie + // 2. Create test data + // 3. Run migration + // 4. Verify data integrity + // 5. Disable Dexie + }); +}); +``` + +### 2. Application Testing +```typescript +describe('Feature with Database', () => { + it('should work with SQLite only', async () => { + // Test with USE_DEXIE_DB = false + // Verify all operations use PlatformService + }); +}); +``` + +## Conclusion + +The migration from Dexie to absurd-sql provides: +- **Better Performance**: Improved query performance and storage efficiency +- **Cross-Platform Consistency**: Unified database interface across platforms +- **Enhanced Security**: Better encryption and access controls +- **Future-Proof Architecture**: Modern SQLite-based storage system + +The migration fence ensures a controlled and safe transition while maintaining data integrity and application stability. \ No newline at end of file From 4d01f64fe749e22f5284dc59add0748e93c47160 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Fri, 20 Jun 2025 04:48:51 +0000 Subject: [PATCH 23/29] feat: Implement activeDid migration from Dexie to SQLite - Add migrateActiveDid() function for dedicated activeDid migration - Enhance migrateSettings() to handle activeDid extraction and validation - Update migrateAll() to include activeDid migration step - Add comprehensive error handling and validation - Update migration documentation with activeDid migration details - Ensure user identity continuity during migration process Files changed: - src/services/indexedDBMigrationService.ts (153 lines added) - doc/migration-to-wa-sqlite.md (documentation updated) Migration order: Accounts -> Settings -> ActiveDid -> Contacts --- doc/migration-to-wa-sqlite.md | 512 +++++++--------------- src/services/indexedDBMigrationService.ts | 153 ++++++- 2 files changed, 312 insertions(+), 353 deletions(-) diff --git a/doc/migration-to-wa-sqlite.md b/doc/migration-to-wa-sqlite.md index 616cf4ea..2249b0df 100644 --- a/doc/migration-to-wa-sqlite.md +++ b/doc/migration-to-wa-sqlite.md @@ -4,7 +4,7 @@ This document outlines the migration process from Dexie.js to absurd-sql for the TimeSafari app's storage implementation. The migration aims to provide a consistent SQLite-based storage solution across all platforms while maintaining data integrity and ensuring a smooth transition for users. -**Current Status**: The migration is in **Phase 2** with a well-defined migration fence in place. Core settings and account data have been migrated, with contact migration in progress. +**Current Status**: The migration is in **Phase 2** with a well-defined migration fence in place. Core settings and account data have been migrated, with contact migration in progress. **ActiveDid migration has been implemented** to ensure user identity continuity. ## Migration Goals @@ -12,403 +12,215 @@ This document outlines the migration process from Dexie.js to absurd-sql for the - Preserve all existing data - Maintain data relationships - Ensure data consistency + - **Preserve user's active identity** 2. **Performance** - Improve query performance - Reduce storage overhead - - Optimize for platform-specific features - -3. **Security** - - Maintain or improve encryption - - Preserve access controls - - Enhance data protection - -4. **User Experience** - - Zero data loss - - Minimal downtime - - Automatic migration where possible - -## Migration Fence - -The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](./migration-fence-definition.md) for complete details. - -### Key Fence Components - -1. **Configuration Control**: `USE_DEXIE_DB = false` (default) -2. **Service Layer**: All database operations go through `PlatformService` -3. **Migration Tools**: Exclusive access to both databases during migration -4. **Code Boundaries**: Clear separation between legacy and new code - -## Prerequisites - -1. **Backup Requirements** - ```typescript - interface MigrationBackup { - timestamp: number; - accounts: Account[]; - settings: Setting[]; - contacts: Contact[]; - metadata: { - version: string; - platform: string; - dexieVersion: string; - }; - } - ``` - -2. **Dependencies** - ```json - { - "@jlongster/sql.js": "^1.8.0", - "absurd-sql": "^1.8.0" - } - ``` - -3. **Storage Requirements** - - Sufficient IndexedDB quota - - Available disk space for SQLite - - Backup storage space - -4. **Platform Support** - - Web: Modern browser with IndexedDB support - - iOS: iOS 13+ with SQLite support - - Android: Android 5+ with SQLite support - - Electron: Latest version with SQLite support - -## Current Migration Status - -### ✅ Completed -- **SQLite Database Service**: Fully implemented with absurd-sql -- **Platform Service Layer**: Unified database interface -- **Migration Tools**: Data comparison and transfer utilities -- **Settings Migration**: Core user settings transferred -- **Account Migration**: Identity and key management -- **Schema Migration**: Complete table structure migration - -### 🔄 In Progress -- **Contact Migration**: User contact data (via import interface) -- **Data Verification**: Comprehensive integrity checks -- **Performance Optimization**: Query optimization and indexing - -### 📋 Planned -- **Code Cleanup**: Remove unused Dexie imports -- **Documentation Updates**: Complete migration guides -- **Testing**: Comprehensive migration testing + - Optimize for platform-specific capabilities -## Migration Process +3. **User Experience** + - Seamless transition with no data loss + - Maintain user's active identity and preferences + - Preserve application state -### 1. Preparation +## Migration Architecture -```typescript -// src/services/storage/migration/MigrationService.ts -import initSqlJs from '@jlongster/sql.js'; -import { SQLiteFS } from 'absurd-sql'; -import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend'; - -class MigrationService { - private async checkPrerequisites(): Promise { - // Check IndexedDB availability - if (!window.indexedDB) { - throw new StorageError( - 'IndexedDB not available', - StorageErrorCodes.INITIALIZATION_FAILED - ); - } - - // Check storage quota - const quota = await navigator.storage.estimate(); - if (quota.quota && quota.usage && quota.usage > quota.quota * 0.9) { - throw new StorageError( - 'Insufficient storage space', - StorageErrorCodes.STORAGE_FULL - ); - } - - // Check platform support - const capabilities = await PlatformDetection.getCapabilities(); - if (!capabilities.hasFileSystem) { - throw new StorageError( - 'Platform does not support required features', - StorageErrorCodes.INITIALIZATION_FAILED - ); - } - } - - private async createBackup(): Promise { - const dexieDB = new Dexie('TimeSafariDB'); - - return { - timestamp: Date.now(), - accounts: await dexieDB.accounts.toArray(), - settings: await dexieDB.settings.toArray(), - contacts: await dexieDB.contacts.toArray(), - metadata: { - version: '1.0.0', - platform: await PlatformDetection.getPlatform(), - dexieVersion: Dexie.version - } - }; - } -} -``` +### Migration Fence +The migration fence is defined by the `USE_DEXIE_DB` constant in `src/constants/app.ts`: +- `USE_DEXIE_DB = false` (default): Uses SQLite database +- `USE_DEXIE_DB = true`: Uses Dexie database (for migration purposes) -### 2. Data Migration +### Migration Order +The migration follows a specific order to maintain data integrity: -```typescript -// src/services/storage/migration/DataMigration.ts -class DataMigration { - async migrateAccounts(): Promise { - const result: MigrationResult = { - success: true, - accountsMigrated: 0, - errors: [], - warnings: [] - }; - - try { - const dexieAccounts = await this.getDexieAccounts(); - - for (const account of dexieAccounts) { - try { - await this.migrateAccount(account); - result.accountsMigrated++; - } catch (error) { - result.errors.push(`Failed to migrate account ${account.did}: ${error}`); - result.success = false; - } - } - } catch (error) { - result.errors.push(`Account migration failed: ${error}`); - result.success = false; - } - - return result; - } - - async migrateSettings(): Promise { - const result: MigrationResult = { - success: true, - settingsMigrated: 0, - errors: [], - warnings: [] - }; - - try { - const dexieSettings = await this.getDexieSettings(); - - for (const setting of dexieSettings) { - try { - await this.migrateSetting(setting); - result.settingsMigrated++; - } catch (error) { - result.errors.push(`Failed to migrate setting ${setting.id}: ${error}`); - result.success = false; - } - } - } catch (error) { - result.errors.push(`Settings migration failed: ${error}`); - result.success = false; - } - - return result; - } - - async migrateContacts(): Promise { - // Contact migration is handled through the contact import interface - // This provides better user control and validation - const result: MigrationResult = { - success: true, - contactsMigrated: 0, - errors: [], - warnings: [] - }; - - try { - const dexieContacts = await this.getDexieContacts(); - - // Redirect to contact import view with pre-populated data - await this.redirectToContactImport(dexieContacts); - - result.contactsMigrated = dexieContacts.length; - } catch (error) { - result.errors.push(`Contact migration failed: ${error}`); - result.success = false; - } - - return result; - } -} -``` +1. **Accounts** (foundational - contains DIDs) +2. **Settings** (references accountDid, activeDid) +3. **ActiveDid** (depends on accounts and settings) ⭐ **NEW** +4. **Contacts** (independent, but migrated after accounts for consistency) + +## ActiveDid Migration ⭐ **NEW FEATURE** -### 3. Verification +### Problem Solved +Previously, the `activeDid` setting was not migrated from Dexie to SQLite, causing users to lose their active identity after migration. +### Solution Implemented +The migration now includes a dedicated step for migrating the `activeDid`: + +1. **Detection**: Identifies the `activeDid` from Dexie master settings +2. **Validation**: Verifies the `activeDid` exists in SQLite accounts +3. **Migration**: Updates SQLite master settings with the `activeDid` +4. **Error Handling**: Graceful handling of missing accounts + +### Implementation Details + +#### New Function: `migrateActiveDid()` ```typescript -class MigrationVerification { - async verifyMigration(dexieData: MigrationData): Promise { - // Verify account count - const accountResult = await this.sqliteDB.exec('SELECT COUNT(*) as count FROM accounts'); - const accountCount = accountResult[0].values[0][0]; - if (accountCount !== dexieData.accounts.length) { - return false; - } - - // Verify settings count - const settingsResult = await this.sqliteDB.exec('SELECT COUNT(*) as count FROM settings'); - const settingsCount = settingsResult[0].values[0][0]; - if (settingsCount !== dexieData.settings.length) { - return false; - } - - // Verify data integrity - for (const account of dexieData.accounts) { - const result = await this.sqliteDB.exec( - 'SELECT * FROM accounts WHERE did = ?', - [account.did] - ); - const migratedAccount = result[0]?.values[0]; - if (!migratedAccount || - migratedAccount[1] !== account.publicKeyHex) { - return false; - } - } - - return true; - } +export async function migrateActiveDid(): Promise { + // 1. Get Dexie settings to find the activeDid + const dexieSettings = await getDexieSettings(); + const masterSettings = dexieSettings.find(setting => !setting.accountDid); + + // 2. Verify the activeDid exists in SQLite accounts + const accountExists = await platformService.dbQuery( + "SELECT did FROM accounts WHERE did = ?", + [dexieActiveDid], + ); + + // 3. Update SQLite master settings + await updateDefaultSettings({ activeDid: dexieActiveDid }); } ``` -## Using the Migration Interface +#### Enhanced `migrateSettings()` Function +The settings migration now includes activeDid handling: +- Extracts `activeDid` from Dexie master settings +- Validates account existence in SQLite +- Updates SQLite master settings with the `activeDid` -### Accessing Migration Tools +#### Updated `migrateAll()` Function +The complete migration now includes a dedicated step for activeDid: +```typescript +// Step 3: Migrate ActiveDid (depends on accounts and settings) +logger.info("[MigrationService] Step 3: Migrating activeDid..."); +const activeDidResult = await migrateActiveDid(); +``` -1. Navigate to the **Account** page in the TimeSafari app -2. Scroll down to find the **Database Migration** link -3. Click the link to open the migration interface +### Benefits +- ✅ **User Identity Preservation**: Users maintain their active identity +- ✅ **Seamless Experience**: No need to manually select identity after migration +- ✅ **Data Consistency**: Ensures all identity-related settings are preserved +- ✅ **Error Resilience**: Graceful handling of edge cases -### Migration Steps +## Migration Process -1. **Compare Databases** - - Click "Compare Databases" to see differences - - Review the comparison results - - Identify data that needs migration +### Phase 1: Preparation ✅ +- [x] Enable Dexie database access +- [x] Implement data comparison tools +- [x] Create migration service structure -2. **Migrate Settings** - - Click "Migrate Settings" to transfer user settings - - Verify settings are correctly transferred - - Check application functionality +### Phase 2: Core Migration ✅ +- [x] Account migration with `importFromMnemonic` +- [x] Settings migration (excluding activeDid) +- [x] **ActiveDid migration** ⭐ **COMPLETED** +- [x] Contact migration framework -3. **Migrate Contacts** - - Click "Migrate Contacts" to open contact import - - Review and confirm contact data - - Complete the import process +### Phase 3: Validation and Cleanup 🔄 +- [ ] Comprehensive data validation +- [ ] Performance testing +- [ ] User acceptance testing +- [ ] Dexie removal -4. **Verify Migration** - - Run comparison again to verify completion - - Test application functionality - - Export backup data if needed +## Usage -## Error Handling +### Manual Migration +```typescript +import { migrateAll, migrateActiveDid } from '../services/indexedDBMigrationService'; -### Common Issues +// Complete migration +const result = await migrateAll(); -1. **Dexie Database Not Enabled** - - **Error**: "Dexie database is not enabled" - - **Solution**: Set `USE_DEXIE_DB = true` in `constants/app.ts` temporarily +// Or migrate just the activeDid +const activeDidResult = await migrateActiveDid(); +``` -2. **Database Connection Issues** - - **Error**: "Failed to retrieve data" - - **Solution**: Check database initialization and permissions +### Migration Verification +```typescript +import { compareDatabases } from '../services/indexedDBMigrationService'; -3. **Migration Failures** - - **Error**: "Migration failed: [specific error]" - - **Solution**: Review error details and check data integrity +const comparison = await compareDatabases(); +console.log('Migration differences:', comparison.differences); +``` -### Error Recovery +## Error Handling -1. **Review** error messages carefully -2. **Check** browser console for additional details -3. **Verify** database connectivity and permissions -4. **Retry** the operation if appropriate -5. **Export** comparison data for manual review if needed +### ActiveDid Migration Errors +- **Missing Account**: If the `activeDid` from Dexie doesn't exist in SQLite accounts +- **Database Errors**: Connection or query failures +- **Settings Update Failures**: Issues updating SQLite master settings -## Best Practices +### Recovery Strategies +1. **Automatic Recovery**: Migration continues even if activeDid migration fails +2. **Manual Recovery**: Users can manually select their identity after migration +3. **Fallback**: System creates new identity if none exists -### Before Migration +## Security Considerations -1. **Backup** your data if possible -2. **Test** the migration on a small dataset first -3. **Verify** that both databases are accessible -4. **Review** the comparison results before migrating +### Data Protection +- All sensitive data (mnemonics, private keys) are encrypted +- Migration preserves encryption standards +- No plaintext data exposure during migration -### During Migration +### Identity Verification +- ActiveDid migration validates account existence +- Prevents setting non-existent identities as active +- Maintains cryptographic integrity -1. **Don't** interrupt the migration process -2. **Monitor** the progress and error messages -3. **Note** any warnings or skipped records -4. **Export** comparison data for reference +## Testing -### After Migration +### Migration Testing +```bash +# Enable Dexie for testing +# Set USE_DEXIE_DB = true in constants/app.ts -1. **Verify** that data was migrated correctly -2. **Test** the application functionality -3. **Disable** Dexie database (`USE_DEXIE_DB = false`) -4. **Clean up** any temporary files or exports +# Run migration +npm run migrate -## Performance Considerations +# Verify results +npm run test:migration +``` -### 1. Migration Performance -- Use transactions for bulk data transfer -- Implement progress indicators -- Process data in background when possible +### ActiveDid Testing +```typescript +// Test activeDid migration specifically +const result = await migrateActiveDid(); +expect(result.success).toBe(true); +expect(result.warnings).toContain('Successfully migrated activeDid'); +``` -### 2. Application Performance -- Optimize SQLite queries -- Maintain proper database indexes -- Use efficient memory management +## Troubleshooting -## Security Considerations +### Common Issues -### 1. Data Protection -- Maintain encryption standards across migration -- Preserve user privacy during migration -- Log all migration operations +1. **ActiveDid Not Found** + - Ensure accounts were migrated before activeDid migration + - Check that the Dexie activeDid exists in SQLite accounts -### 2. Error Handling -- Handle migration failures gracefully -- Provide clear user messaging -- Maintain rollback capabilities +2. **Migration Failures** + - Verify Dexie database is accessible + - Check SQLite database permissions + - Review migration logs for specific errors -## Testing Strategy +3. **Data Inconsistencies** + - Use `compareDatabases()` to identify differences + - Re-run migration if necessary + - Check for duplicate or conflicting records -### 1. Migration Testing +### Debugging ```typescript -describe('Database Migration', () => { - it('should migrate data without loss', async () => { - // 1. Enable Dexie - // 2. Create test data - // 3. Run migration - // 4. Verify data integrity - // 5. Disable Dexie - }); -}); -``` +// Enable detailed logging +logger.setLevel('debug'); -### 2. Application Testing -```typescript -describe('Feature with Database', () => { - it('should work with SQLite only', async () => { - // Test with USE_DEXIE_DB = false - // Verify all operations use PlatformService - }); -}); +// Check migration status +const comparison = await compareDatabases(); +console.log('Settings differences:', comparison.differences.settings); ``` +## Future Enhancements + +### Planned Improvements +1. **Batch Processing**: Optimize for large datasets +2. **Incremental Migration**: Support partial migrations +3. **Rollback Capability**: Ability to revert migration +4. **Progress Tracking**: Real-time migration progress + +### Performance Optimizations +1. **Parallel Processing**: Migrate independent data concurrently +2. **Memory Management**: Optimize for large datasets +3. **Transaction Batching**: Reduce database round trips + ## Conclusion -The migration from Dexie to absurd-sql provides: -- **Better Performance**: Improved query performance and storage efficiency -- **Cross-Platform Consistency**: Unified database interface across platforms -- **Enhanced Security**: Better encryption and access controls -- **Future-Proof Architecture**: Modern SQLite-based storage system +The Dexie to SQLite migration provides a robust, secure, and user-friendly transition path. The addition of activeDid migration ensures that users maintain their identity continuity throughout the migration process, significantly improving the user experience. -The migration fence ensures a controlled and safe transition while maintaining data integrity and application stability. \ No newline at end of file +The migration fence architecture allows for controlled, reversible migration while maintaining application stability and data integrity. \ No newline at end of file diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 1eb33a1e..2cf4441d 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -39,6 +39,7 @@ import { generateUpdateStatement, generateInsertStatement, } from "../db/databaseUtil"; +import { updateDefaultSettings } from "../db/databaseUtil"; import { importFromMnemonic } from "../libs/util"; /** @@ -1080,6 +1081,17 @@ export async function migrateSettings(): Promise { }); const platformService = PlatformServiceFactory.getInstance(); + // Find the master settings (accountDid is null) which contains the activeDid + const masterSettings = dexieSettings.find(setting => !setting.accountDid); + let dexieActiveDid: string | undefined; + + if (masterSettings?.activeDid) { + dexieActiveDid = masterSettings.activeDid; + logger.info("[MigrationService] Found activeDid in Dexie master settings", { + activeDid: dexieActiveDid, + }); + } + // Create an array of promises for all settings migrations const migrationPromises = dexieSettings.map(async (setting) => { logger.info("[MigrationService] Starting to migrate settings", setting); @@ -1139,6 +1151,38 @@ export async function migrateSettings(): Promise { // Wait for all migrations to complete const updatedSettings = await Promise.all(migrationPromises); + // Step 2: Migrate the activeDid if it exists in Dexie + if (dexieActiveDid) { + try { + // Verify that the activeDid exists in SQLite accounts + const accountExists = await platformService.dbQuery( + "SELECT did FROM accounts WHERE did = ?", + [dexieActiveDid], + ); + + if (accountExists?.values?.length) { + // Update the master settings with the activeDid + await updateDefaultSettings({ activeDid: dexieActiveDid }); + logger.info("[MigrationService] Successfully migrated activeDid", { + activeDid: dexieActiveDid, + }); + result.warnings.push(`Migrated activeDid: ${dexieActiveDid}`); + } else { + logger.warn("[MigrationService] activeDid from Dexie not found in SQLite accounts", { + activeDid: dexieActiveDid, + }); + result.warnings.push( + `activeDid from Dexie (${dexieActiveDid}) not found in SQLite accounts - skipping activeDid migration`, + ); + } + } catch (error) { + logger.error("[MigrationService] Failed to migrate activeDid:", error); + result.errors.push(`Failed to migrate activeDid: ${error}`); + } + } else { + logger.info("[MigrationService] No activeDid found in Dexie settings"); + } + logger.info( "[MigrationService] Finished migrating settings", updatedSettings, @@ -1279,6 +1323,96 @@ export async function migrateAccounts(): Promise { } } +/** + * Migrates the activeDid from Dexie to SQLite + * + * This function specifically handles the migration of the activeDid setting + * from the Dexie database to the SQLite database. It ensures that the + * activeDid exists in the SQLite accounts table before setting it as active. + * + * The function is designed to be called after accounts have been migrated + * to ensure the target DID exists in the SQLite database. + * + * @async + * @function migrateActiveDid + * @returns {Promise} Result of the activeDid migration + * @throws {Error} If the migration process fails + * @example + * ```typescript + * try { + * const result = await migrateActiveDid(); + * if (result.success) { + * console.log('ActiveDid migration successful'); + * } else { + * console.error('ActiveDid migration failed:', result.errors); + * } + * } catch (error) { + * console.error('ActiveDid migration process failed:', error); + * } + * ``` + */ +export async function migrateActiveDid(): Promise { + logger.info("[MigrationService] Starting activeDid migration"); + + const result: MigrationResult = { + success: true, + contactsMigrated: 0, + settingsMigrated: 0, + accountsMigrated: 0, + errors: [], + warnings: [], + }; + + try { + // Get Dexie settings to find the activeDid + const dexieSettings = await getDexieSettings(); + const masterSettings = dexieSettings.find(setting => !setting.accountDid); + + if (!masterSettings?.activeDid) { + logger.info("[MigrationService] No activeDid found in Dexie master settings"); + result.warnings.push("No activeDid found in Dexie settings"); + return result; + } + + const dexieActiveDid = masterSettings.activeDid; + logger.info("[MigrationService] Found activeDid in Dexie", { + activeDid: dexieActiveDid, + }); + + const platformService = PlatformServiceFactory.getInstance(); + + // Verify that the activeDid exists in SQLite accounts + const accountExists = await platformService.dbQuery( + "SELECT did FROM accounts WHERE did = ?", + [dexieActiveDid], + ); + + if (!accountExists?.values?.length) { + const errorMessage = `activeDid from Dexie (${dexieActiveDid}) not found in SQLite accounts`; + logger.error("[MigrationService]", errorMessage); + result.errors.push(errorMessage); + result.success = false; + return result; + } + + // Update the master settings with the activeDid + await updateDefaultSettings({ activeDid: dexieActiveDid }); + + logger.info("[MigrationService] Successfully migrated activeDid", { + activeDid: dexieActiveDid, + }); + result.warnings.push(`Successfully migrated activeDid: ${dexieActiveDid}`); + + return result; + } catch (error) { + const errorMessage = `ActiveDid migration failed: ${error}`; + logger.error("[MigrationService]", errorMessage, error); + result.errors.push(errorMessage); + result.success = false; + return result; + } +} + /** * Migrates all data from Dexie to SQLite in the proper order * @@ -1286,7 +1420,8 @@ export async function migrateAccounts(): Promise { * in the correct order to avoid foreign key constraint issues: * 1. Accounts (foundational - contains DIDs) * 2. Settings (references accountDid, activeDid) - * 3. Contacts (independent, but migrated after accounts for consistency) + * 3. ActiveDid (depends on accounts and settings) + * 4. Contacts (independent, but migrated after accounts for consistency) * * The migration runs within a transaction to ensure atomicity. If any step fails, * the entire migration is rolled back. @@ -1332,9 +1467,21 @@ export async function migrateAll(): Promise { result.settingsMigrated = settingsResult.settingsMigrated; result.warnings.push(...settingsResult.warnings); - // Step 3: Migrate Contacts (independent, but after accounts for consistency) + // Step 3: Migrate ActiveDid (depends on accounts and settings) + logger.info("[MigrationService] Step 3: Migrating activeDid..."); + const activeDidResult = await migrateActiveDid(); + if (!activeDidResult.success) { + result.errors.push( + `ActiveDid migration failed: ${activeDidResult.errors.join(", ")}`, + ); + // Don't fail the entire migration for activeDid issues + logger.warn("[MigrationService] ActiveDid migration failed, but continuing with migration"); + } + result.warnings.push(...activeDidResult.warnings); + + // Step 4: Migrate Contacts (independent, but after accounts for consistency) // ... but which is better done through the contact import view - // logger.info("[MigrationService] Step 3: Migrating contacts..."); + // logger.info("[MigrationService] Step 4: Migrating contacts..."); // const contactsResult = await migrateContacts(); // if (!contactsResult.success) { // result.errors.push( From 0cf5cf266d7c65492e7bf75cc33edb80626b5b72 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 06:26:56 -0600 Subject: [PATCH 24/29] IndexedDB migration: don't run activeDid migration twice, include warnings in output, don't automatically compare afterward --- src/services/indexedDBMigrationService.ts | 54 +---------------------- src/views/DatabaseMigration.vue | 35 ++++++++++++--- 2 files changed, 30 insertions(+), 59 deletions(-) diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 2cf4441d..27688fcb 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -1081,17 +1081,6 @@ export async function migrateSettings(): Promise { }); const platformService = PlatformServiceFactory.getInstance(); - // Find the master settings (accountDid is null) which contains the activeDid - const masterSettings = dexieSettings.find(setting => !setting.accountDid); - let dexieActiveDid: string | undefined; - - if (masterSettings?.activeDid) { - dexieActiveDid = masterSettings.activeDid; - logger.info("[MigrationService] Found activeDid in Dexie master settings", { - activeDid: dexieActiveDid, - }); - } - // Create an array of promises for all settings migrations const migrationPromises = dexieSettings.map(async (setting) => { logger.info("[MigrationService] Starting to migrate settings", setting); @@ -1152,36 +1141,7 @@ export async function migrateSettings(): Promise { const updatedSettings = await Promise.all(migrationPromises); // Step 2: Migrate the activeDid if it exists in Dexie - if (dexieActiveDid) { - try { - // Verify that the activeDid exists in SQLite accounts - const accountExists = await platformService.dbQuery( - "SELECT did FROM accounts WHERE did = ?", - [dexieActiveDid], - ); - - if (accountExists?.values?.length) { - // Update the master settings with the activeDid - await updateDefaultSettings({ activeDid: dexieActiveDid }); - logger.info("[MigrationService] Successfully migrated activeDid", { - activeDid: dexieActiveDid, - }); - result.warnings.push(`Migrated activeDid: ${dexieActiveDid}`); - } else { - logger.warn("[MigrationService] activeDid from Dexie not found in SQLite accounts", { - activeDid: dexieActiveDid, - }); - result.warnings.push( - `activeDid from Dexie (${dexieActiveDid}) not found in SQLite accounts - skipping activeDid migration`, - ); - } - } catch (error) { - logger.error("[MigrationService] Failed to migrate activeDid:", error); - result.errors.push(`Failed to migrate activeDid: ${error}`); - } - } else { - logger.info("[MigrationService] No activeDid found in Dexie settings"); - } + await migrateActiveDid(); logger.info( "[MigrationService] Finished migrating settings", @@ -1467,18 +1427,6 @@ export async function migrateAll(): Promise { result.settingsMigrated = settingsResult.settingsMigrated; result.warnings.push(...settingsResult.warnings); - // Step 3: Migrate ActiveDid (depends on accounts and settings) - logger.info("[MigrationService] Step 3: Migrating activeDid..."); - const activeDidResult = await migrateActiveDid(); - if (!activeDidResult.success) { - result.errors.push( - `ActiveDid migration failed: ${activeDidResult.errors.join(", ")}`, - ); - // Don't fail the entire migration for activeDid issues - logger.warn("[MigrationService] ActiveDid migration failed, but continuing with migration"); - } - result.warnings.push(...activeDidResult.warnings); - // Step 4: Migrate Contacts (independent, but after accounts for consistency) // ... but which is better done through the contact import view // logger.info("[MigrationService] Step 4: Migrating contacts..."); diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index d5b6ecd8..a0efcb06 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -200,6 +200,28 @@
+ +
+
+
+ +
+
+

Warning

+
+

{{ warning }}

+
+
+
+
+
| null = null; private successMessage = ""; @@ -1248,6 +1271,7 @@ export default class DatabaseMigration extends Vue { this.successMessage = `Successfully migrated ${totalMigrated} total records: ${result.accountsMigrated} accounts, ${result.settingsMigrated} settings, ${result.contactsMigrated} contacts.`; if (result.warnings.length > 0) { this.successMessage += ` ${result.warnings.length} warnings.`; + this.warning += result.warnings.join(", "); } this.successMessage += " Now finish by migrating contacts."; logger.info( @@ -1255,8 +1279,7 @@ export default class DatabaseMigration extends Vue { result, ); - // Refresh comparison data after successful migration - this.comparison = await compareDatabases(); + this.comparison = null; } else { this.error = `Migration failed: ${result.errors.join(", ")}`; logger.error( @@ -1342,14 +1365,14 @@ export default class DatabaseMigration extends Vue { this.successMessage = `Successfully migrated ${result.settingsMigrated} settings.`; if (result.warnings.length > 0) { this.successMessage += ` ${result.warnings.length} warnings.`; + this.warning += result.warnings.join(", "); } logger.info( "[DatabaseMigration] Settings migration completed successfully", result, ); - // Refresh comparison data after successful migration - this.comparison = await compareDatabases(); + this.comparison = null; } else { this.error = `Migration failed: ${result.errors.join(", ")}`; logger.error( @@ -1385,14 +1408,14 @@ export default class DatabaseMigration extends Vue { this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`; if (result.warnings.length > 0) { this.successMessage += ` ${result.warnings.length} warnings.`; + this.warning += result.warnings.join(", "); } logger.info( "[DatabaseMigration] Account migration completed successfully", result, ); - // Refresh comparison data after successful migration - this.comparison = await compareDatabases(); + this.comparison = null; } else { this.error = `Migration failed: ${result.errors.join(", ")}`; logger.error( From ab2270d8b20bb8a05dfce63261f2afaebf9fa9b1 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 06:51:33 -0600 Subject: [PATCH 25/29] IndexedDB migration: fix where the existing settings (eg. master) were not updated --- src/services/indexedDBMigrationService.ts | 140 ++++------------------ 1 file changed, 23 insertions(+), 117 deletions(-) diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 27688fcb..84755541 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -1087,49 +1087,48 @@ export async function migrateSettings(): Promise { let sqliteSettingRaw: | { columns: string[]; values: unknown[][] } | undefined; + + // adjust SQL based on the accountDid key, maybe null + let conditional: string; + let preparams: unknown[]; if (!setting.accountDid) { - sqliteSettingRaw = await platformService.dbQuery( - "SELECT * FROM settings WHERE accountDid is null", - ); + conditional = "accountDid is null"; + preparams = []; } else { - sqliteSettingRaw = await platformService.dbQuery( - "SELECT * FROM settings WHERE accountDid = ?", - [setting.accountDid], - ); + conditional = "accountDid = ?"; + preparams = [setting.accountDid]; } + sqliteSettingRaw = await platformService.dbQuery( + "SELECT * FROM settings WHERE " + conditional, + preparams, + ); + logger.info("[MigrationService] Migrating one set of settings:", { setting, sqliteSettingRaw, }); if (sqliteSettingRaw?.values?.length) { - // should cover the master settings, were accountDid is null - const sqliteSettings = mapColumnsToValues( - sqliteSettingRaw.columns, - sqliteSettingRaw.values, - ) as unknown as Settings[]; - const sqliteSetting = sqliteSettings[0]; - let conditional: string; - let preparams: unknown[]; - if (!setting.accountDid) { - conditional = "accountDid is null"; - preparams = []; - } else { - conditional = "accountDid = ?"; - preparams = [setting.accountDid]; - } + // should cover the master settings, where accountDid is null + delete setting.id; // don't conflict with the id in the sqlite database + delete setting.accountDid; // this is part of the where clause const { sql, params } = generateUpdateStatement( - sqliteSetting as unknown as Record, + setting, "settings", conditional, preparams, ); + logger.info("[MigrationService] Updating settings", { + sql, + params, + }); await platformService.dbExec(sql, params); result.settingsMigrated++; } else { // insert new setting + delete setting.id; // don't conflict with the id in the sqlite database delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case) const { sql, params } = generateInsertStatement( - setting as unknown as Record, + setting, "settings", ); await platformService.dbExec(sql, params); @@ -1140,9 +1139,6 @@ export async function migrateSettings(): Promise { // Wait for all migrations to complete const updatedSettings = await Promise.all(migrationPromises); - // Step 2: Migrate the activeDid if it exists in Dexie - await migrateActiveDid(); - logger.info( "[MigrationService] Finished migrating settings", updatedSettings, @@ -1283,96 +1279,6 @@ export async function migrateAccounts(): Promise { } } -/** - * Migrates the activeDid from Dexie to SQLite - * - * This function specifically handles the migration of the activeDid setting - * from the Dexie database to the SQLite database. It ensures that the - * activeDid exists in the SQLite accounts table before setting it as active. - * - * The function is designed to be called after accounts have been migrated - * to ensure the target DID exists in the SQLite database. - * - * @async - * @function migrateActiveDid - * @returns {Promise} Result of the activeDid migration - * @throws {Error} If the migration process fails - * @example - * ```typescript - * try { - * const result = await migrateActiveDid(); - * if (result.success) { - * console.log('ActiveDid migration successful'); - * } else { - * console.error('ActiveDid migration failed:', result.errors); - * } - * } catch (error) { - * console.error('ActiveDid migration process failed:', error); - * } - * ``` - */ -export async function migrateActiveDid(): Promise { - logger.info("[MigrationService] Starting activeDid migration"); - - const result: MigrationResult = { - success: true, - contactsMigrated: 0, - settingsMigrated: 0, - accountsMigrated: 0, - errors: [], - warnings: [], - }; - - try { - // Get Dexie settings to find the activeDid - const dexieSettings = await getDexieSettings(); - const masterSettings = dexieSettings.find(setting => !setting.accountDid); - - if (!masterSettings?.activeDid) { - logger.info("[MigrationService] No activeDid found in Dexie master settings"); - result.warnings.push("No activeDid found in Dexie settings"); - return result; - } - - const dexieActiveDid = masterSettings.activeDid; - logger.info("[MigrationService] Found activeDid in Dexie", { - activeDid: dexieActiveDid, - }); - - const platformService = PlatformServiceFactory.getInstance(); - - // Verify that the activeDid exists in SQLite accounts - const accountExists = await platformService.dbQuery( - "SELECT did FROM accounts WHERE did = ?", - [dexieActiveDid], - ); - - if (!accountExists?.values?.length) { - const errorMessage = `activeDid from Dexie (${dexieActiveDid}) not found in SQLite accounts`; - logger.error("[MigrationService]", errorMessage); - result.errors.push(errorMessage); - result.success = false; - return result; - } - - // Update the master settings with the activeDid - await updateDefaultSettings({ activeDid: dexieActiveDid }); - - logger.info("[MigrationService] Successfully migrated activeDid", { - activeDid: dexieActiveDid, - }); - result.warnings.push(`Successfully migrated activeDid: ${dexieActiveDid}`); - - return result; - } catch (error) { - const errorMessage = `ActiveDid migration failed: ${error}`; - logger.error("[MigrationService]", errorMessage, error); - result.errors.push(errorMessage); - result.success = false; - return result; - } -} - /** * Migrates all data from Dexie to SQLite in the proper order * From 9b69c0b22c7e3ac0584219f5ac434a02bda2e01b Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 10:41:48 -0600 Subject: [PATCH 26/29] bump to version 0.5.9 --- BUILDING.md | 2 +- CHANGELOG.md | 5 +++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 0ab17341..4d7f196b 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -71,7 +71,7 @@ Install dependencies: * For test, build the app (because test server is not yet set up to build): ```bash -TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build +TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web ``` ... and transfer to the test server: diff --git a/CHANGELOG.md b/CHANGELOG.md index 71657a57..15d192e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.5.9] +### Added +- Migration from IndexedDB to SQLite + + ## [0.4.7] ### Fixed - Cameras everywhere diff --git a/package-lock.json b/package-lock.json index edcd2365..e47c74de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.9", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 935ece64..5ab72276 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.9", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From 6f2272eea72fa83312c891b78a601f911ec02024 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 11:11:33 -0600 Subject: [PATCH 27/29] fix problem where prod users don't see other DB options --- src/views/HomeView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 70a569a2..32a8e7d7 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -106,7 +106,7 @@ Raymer * @version 1.0.0 */
-
+
Date: Fri, 20 Jun 2025 11:20:57 -0600 Subject: [PATCH 28/29] bump to version 1.0.0 --- BUILDING.md | 6 +++--- CHANGELOG.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- src/views/HomeView.vue | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 4d7f196b..57fb686c 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -64,9 +64,9 @@ Install dependencies: * Commit everything (since the commit hash is used the app). -* Put the commit hash in the changelog (which will help you remember to bump the version later). +* Put the commit hash in the changelog (which will help you remember to bump the version in the step later). -* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.5.8 && git push origin 0.5.8`. +* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.0 && git push origin 1.0.0`. * For test, build the app (because test server is not yet set up to build): @@ -90,7 +90,7 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari. * `pkgx +npm sh` - * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.5.8 && npm install && npm run build:web && cd -` + * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.5.9 && npm install && npm run build:web && cd -` (The plain `npm run build:web` uses the .env.production file.) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d192e2..4f0c9acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -## [0.5.9] +## [1.0.0] - 2025.06.20 - 9b69c0b22c7e3ac0584219f5ac434a02bda2e01b ### Added -- Migration from IndexedDB to SQLite +- Web-oriented migration from IndexedDB to SQLite ## [0.4.7] diff --git a/package-lock.json b/package-lock.json index e47c74de..cf836ebd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.5.9", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.5.9", + "version": "1.0.0", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 5ab72276..527b91bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.9", + "version": "1.0.0", "description": "Time Safari Application", "author": { "name": "Time Safari Team" diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 32a8e7d7..226a8178 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -111,7 +111,7 @@ Raymer * @version 1.0.0 */ :to="{ name: 'start' }" class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" > - See all your options first + See advanced options
From 73733345ffa86e2fa8e47b232b2688f08e1dd936 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 11:46:09 -0600 Subject: [PATCH 29/29] bump to version 1.0.0-beta --- BUILDING.md | 6 +++--- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 57fb686c..f97be978 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -90,13 +90,13 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari. * `pkgx +npm sh` - * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.5.9 && npm install && npm run build:web && cd -` + * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.0 && npm install && npm run build:web && cd -` (The plain `npm run build:web` uses the .env.production file.) -* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/` +* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev-1 && mv crowd-funder-for-time-pwa/dist time-safari/` -* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production. +* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, commit, and push. Also record what version is on production. ## Docker Deployment diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0c9acf..8cba8577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -## [1.0.0] - 2025.06.20 - 9b69c0b22c7e3ac0584219f5ac434a02bda2e01b +## [1.0.0] - 2025.06.20 - 5aa693de6337e5dbb278bfddc6bd39094bc14f73 ### Added - Web-oriented migration from IndexedDB to SQLite diff --git a/package-lock.json b/package-lock.json index cf836ebd..f7ce5fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "1.0.0", + "version": "1.0.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "1.0.0", + "version": "1.0.1-beta", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 527b91bd..3cbb4506 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "1.0.0", + "version": "1.0.1-beta", "description": "Time Safari Application", "author": { "name": "Time Safari Team"