diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index c8688b83..91e98634 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -227,10 +227,28 @@ export async function logConsoleAndDb( } /** - * Generates an SQL INSERT statement and parameters from a model object. - * @param model The model object containing fields to update - * @param tableName The name of the table to update - * @returns Object containing the SQL statement and parameters array + * 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'] + * ``` */ export function generateInsertStatement( model: Record, @@ -248,12 +266,30 @@ export function generateInsertStatement( } /** - * Generates an SQL UPDATE statement and parameters from a model object. - * @param model The model object containing fields to update - * @param tableName The name of the table to update - * @param whereClause The WHERE clause for the update (e.g. "id = ?") - * @param whereParams Parameters for the WHERE clause - * @returns Object containing the SQL statement and parameters array + * 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'] + * ``` */ export function generateUpdateStatement( model: Record, diff --git a/src/libs/util.ts b/src/libs/util.ts index 030ba416..a95a3e4a 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -1021,12 +1021,12 @@ export async function importFromMnemonic( // Create new identifier const newId = newIdentifier(address, publicHex, privateHex, derivationPath); - // Handle database operations - const accountsDB = await accountsDBPromise; + // Handle erasures if (shouldErase) { const platformService = PlatformServiceFactory.getInstance(); await platformService.dbExec("DELETE FROM accounts"); if (USE_DEXIE_DB) { + const accountsDB = await accountsDBPromise; await accountsDB.accounts.clear(); } } diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index ca1dcb35..5aaa1241 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -26,12 +26,11 @@ import "dexie-export-import"; import { PlatformServiceFactory } from "./PlatformServiceFactory"; import { db, accountsDBPromise } from "../db/index"; import { Contact, ContactMethod } from "../db/tables/contacts"; -import { Settings, MASTER_SETTINGS_KEY, SettingsWithJsonStrings, BoundingBox } from "../db/tables/settings"; -import { Account, AccountEncrypted } from "../db/tables/accounts"; +import { Settings, MASTER_SETTINGS_KEY, BoundingBox } from "../db/tables/settings"; +import { Account } from "../db/tables/accounts"; import { logger } from "../utils/logger"; -import { mapColumnsToValues, parseJsonField } from "../db/databaseUtil"; +import { mapColumnsToValues, parseJsonField, generateUpdateStatement, generateInsertStatement } from "../db/databaseUtil"; import { importFromMnemonic } from "../libs/util"; -import { IIdentifier } from "@veramo/core"; /** * Interface for data comparison results between Dexie and SQLite databases @@ -1028,228 +1027,47 @@ export async function migrateSettings( try { const dexieSettings = await getDexieSettings(); const platformService = PlatformServiceFactory.getInstance(); - - // Group settings by DID to handle duplicates - const settingsByDid = new Map(); - - // Organize settings by DID - dexieSettings.forEach(setting => { - const isMasterSetting = setting.id === MASTER_SETTINGS_KEY; - const did = isMasterSetting ? setting.activeDid : setting.accountDid; - - if (!did) { - result.warnings.push(`Setting ${setting.id} has no DID, skipping`); - return; - } - - if (!settingsByDid.has(did)) { - settingsByDid.set(did, {}); - } - - const didSettings = settingsByDid.get(did)!; - if (isMasterSetting) { - didSettings.master = setting; - logger.info("[MigrationService] Found master settings", { - did, - id: setting.id, - firstName: setting.firstName, - isRegistered: setting.isRegistered, - profileImageUrl: setting.profileImageUrl, - showShortcutBvc: setting.showShortcutBvc, - searchBoxes: setting.searchBoxes - }); + // loop through dexieSettings, + // load the one with the matching accountDid from sqlite, + // and if one doesn't exist then insert it, + // otherwise, update the fields + dexieSettings.forEach(async (setting) => { + const sqliteSettingRaw = await platformService.dbQuery( + "SELECT * FROM settings WHERE accountDid = ?", + [setting.accountDid] + ); + if (sqliteSettingRaw?.values?.length) { + // should cover the master settings, were accountDid is null + const sqliteSetting = mapColumnsToValues(sqliteSettingRaw.columns, sqliteSettingRaw.values) as unknown as Settings; + let conditional: string; + let preparams: unknown[]; + if (!setting.accountDid) { + conditional = "accountDid is null"; + preparams = []; + } else { + conditional = "accountDid = ?"; + preparams = [setting.accountDid]; + } + const { sql, params } = generateUpdateStatement( + sqliteSetting as unknown as Record, + "settings", + conditional, + preparams + ); + await platformService.dbExec(sql, params); + result.settingsMigrated++; } else { - didSettings.account = setting; - logger.info("[MigrationService] Found account settings", { - did, - id: setting.id, - firstName: setting.firstName, - isRegistered: setting.isRegistered, - profileImageUrl: setting.profileImageUrl, - showShortcutBvc: setting.showShortcutBvc, - searchBoxes: setting.searchBoxes - }); + // insert new setting + 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, + "settings" + ); + await platformService.dbExec(sql, params); + result.settingsMigrated++; } }); - // Process each unique DID's settings - for (const [did, didSettings] of settingsByDid.entries()) { - try { - // Process master settings - if (didSettings.master) { - const masterData = { - id: MASTER_SETTINGS_KEY, - activeDid: did, - accountDid: "", // Empty for master settings - apiServer: didSettings.master.apiServer || "", - filterFeedByNearby: didSettings.master.filterFeedByNearby || false, - filterFeedByVisible: didSettings.master.filterFeedByVisible || false, - finishedOnboarding: didSettings.master.finishedOnboarding || false, - firstName: didSettings.master.firstName || "", - hideRegisterPromptOnNewContact: didSettings.master.hideRegisterPromptOnNewContact || false, - isRegistered: didSettings.master.isRegistered || false, - lastName: didSettings.master.lastName || "", - profileImageUrl: didSettings.master.profileImageUrl || "", - searchBoxes: didSettings.master.searchBoxes || [], - showShortcutBvc: didSettings.master.showShortcutBvc || false - }; - - // Check if master setting exists - const existingMaster = await platformService.dbQuery( - "SELECT id FROM settings WHERE id = ? AND activeDid = ? AND accountDid = ''", - [MASTER_SETTINGS_KEY, did] - ); - - if (existingMaster?.values?.length) { - logger.info("[MigrationService] Updating master settings", { did, masterData }); - await platformService.dbQuery( - `UPDATE settings SET - activeDid = ?, - accountDid = ?, - firstName = ?, - isRegistered = ?, - profileImageUrl = ?, - showShortcutBvc = ?, - searchBoxes = ? - WHERE id = ?`, - [ - masterData.activeDid, - masterData.accountDid, - masterData.firstName, - masterData.isRegistered, - masterData.profileImageUrl, - masterData.showShortcutBvc, - JSON.stringify(masterData.searchBoxes), - MASTER_SETTINGS_KEY - ] - ); - } else { - logger.info("[MigrationService] Inserting master settings", { did, masterData }); - await platformService.dbQuery( - `INSERT INTO settings ( - id, - activeDid, - accountDid, - firstName, - isRegistered, - profileImageUrl, - showShortcutBvc, - searchBoxes - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [ - MASTER_SETTINGS_KEY, - masterData.activeDid, - masterData.accountDid, - masterData.firstName, - masterData.isRegistered, - masterData.profileImageUrl, - masterData.showShortcutBvc, - JSON.stringify(masterData.searchBoxes) - ] - ); - } - result.settingsMigrated++; - } - - // Process account settings - if (didSettings.account) { - const accountData = { - id: 2, // Account settings always use id 2 - activeDid: "", // Empty for account settings - accountDid: did, - apiServer: didSettings.account.apiServer || "", - filterFeedByNearby: didSettings.account.filterFeedByNearby || false, - filterFeedByVisible: didSettings.account.filterFeedByVisible || false, - finishedOnboarding: didSettings.account.finishedOnboarding || false, - firstName: didSettings.account.firstName || "", - hideRegisterPromptOnNewContact: didSettings.account.hideRegisterPromptOnNewContact || false, - isRegistered: didSettings.account.isRegistered || false, - lastName: didSettings.account.lastName || "", - profileImageUrl: didSettings.account.profileImageUrl || "", - searchBoxes: didSettings.account.searchBoxes || [], - showShortcutBvc: didSettings.account.showShortcutBvc || false - }; - - // Check if account setting exists - const existingAccount = await platformService.dbQuery( - "SELECT id FROM settings WHERE id = ? AND accountDid = ? AND activeDid = ''", - [2, did] - ); - - if (existingAccount?.values?.length) { - logger.info("[MigrationService] Updating account settings", { did, accountData }); - await platformService.dbQuery( - `UPDATE settings SET - activeDid = ?, - accountDid = ?, - firstName = ?, - isRegistered = ?, - profileImageUrl = ?, - showShortcutBvc = ?, - searchBoxes = ? - WHERE id = ?`, - [ - accountData.activeDid, - accountData.accountDid, - accountData.firstName, - accountData.isRegistered, - accountData.profileImageUrl, - accountData.showShortcutBvc, - JSON.stringify(accountData.searchBoxes), - 2 - ] - ); - } else { - logger.info("[MigrationService] Inserting account settings", { did, accountData }); - await platformService.dbQuery( - `INSERT INTO settings ( - id, - activeDid, - accountDid, - firstName, - isRegistered, - profileImageUrl, - showShortcutBvc, - searchBoxes - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [ - 2, - accountData.activeDid, - accountData.accountDid, - accountData.firstName, - accountData.isRegistered, - accountData.profileImageUrl, - accountData.showShortcutBvc, - JSON.stringify(accountData.searchBoxes) - ] - ); - } - result.settingsMigrated++; - } - - logger.info("[MigrationService] Successfully migrated settings for DID", { - did, - masterMigrated: !!didSettings.master, - accountMigrated: !!didSettings.account - }); - - } catch (error) { - const errorMessage = `Failed to migrate settings for DID ${did}: ${error}`; - result.errors.push(errorMessage); - logger.error("[MigrationService] Settings migration failed:", { - error, - did - }); - } - } - - if (result.errors.length > 0) { - result.success = false; - } - return result; } catch (error) { const errorMessage = `Settings migration failed: ${error}`; @@ -1277,7 +1095,6 @@ export async function migrateSettings( * * @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 @@ -1294,12 +1111,8 @@ export async function migrateSettings( * } * ``` */ -export async function migrateAccounts( - overwriteExisting: boolean = false, -): Promise { - logger.info("[MigrationService] Starting account migration", { - overwriteExisting, - }); +export async function migrateAccounts(): Promise { + logger.info("[MigrationService] Starting account migration"); const result: MigrationResult = { success: true, @@ -1335,67 +1148,17 @@ export async function migrateAccounts( [did] ); - if (existingResult?.values?.length && !overwriteExisting) { + if (existingResult?.values?.length) { result.warnings.push(`Account with DID ${did} already exists, skipping`); continue; } - - // Map Dexie fields to SQLite fields - const accountData = { - did: account.did, - dateCreated: account.dateCreated, - derivationPath: account.derivationPath || "", - identityEncrBase64: account.identity || "", - mnemonicEncrBase64: account.mnemonic || "", - passkeyCredIdHex: account.passkeyCredIdHex || "", - publicKeyHex: account.publicKeyHex || "" - }; - - // Insert or update the account - if (existingResult?.values?.length) { - await platformService.dbQuery( - `UPDATE accounts SET - dateCreated = ?, - derivationPath = ?, - identityEncrBase64 = ?, - mnemonicEncrBase64 = ?, - passkeyCredIdHex = ?, - publicKeyHex = ? - WHERE did = ?`, - [ - accountData.dateCreated, - accountData.derivationPath, - accountData.identityEncrBase64, - accountData.mnemonicEncrBase64, - accountData.passkeyCredIdHex, - accountData.publicKeyHex, - did - ] - ); + if (account.mnemonic) { + await importFromMnemonic(account.mnemonic, account.derivationPath); + result.accountsMigrated++; } else { - await platformService.dbQuery( - `INSERT INTO accounts ( - did, - dateCreated, - derivationPath, - identityEncrBase64, - mnemonicEncrBase64, - passkeyCredIdHex, - publicKeyHex - ) VALUES (?, ?, ?, ?, ?, ?, ?)`, - [ - did, - accountData.dateCreated, - accountData.derivationPath, - accountData.identityEncrBase64, - accountData.mnemonicEncrBase64, - accountData.passkeyCredIdHex, - accountData.publicKeyHex - ] - ); + result.errors.push(`Account with DID ${did} has no mnemonic, skipping`); } - result.accountsMigrated++; logger.info("[MigrationService] Successfully migrated account", { did, dateCreated: account.dateCreated @@ -1424,99 +1187,6 @@ export async function migrateAccounts( } } -/** - * 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], - }; -} - /** * Migrates all data from Dexie to SQLite in the proper order * @@ -1551,7 +1221,7 @@ export async function migrateAll( // Step 1: Migrate Accounts (foundational) logger.info("[MigrationService] Step 1: Migrating accounts..."); - const accountsResult = await migrateAccounts(overwriteExisting); + const accountsResult = await migrateAccounts(); if (!accountsResult.success) { result.errors.push( `Account migration failed: ${accountsResult.errors.join(", ")}`, diff --git a/src/views/ContactImportView.vue b/src/views/ContactImportView.vue index f933aee4..294a45fe 100644 --- a/src/views/ContactImportView.vue +++ b/src/views/ContactImportView.vue @@ -566,7 +566,7 @@ export default class ContactImportView extends Vue { this.checkingImports = true; try { - const jwt: string = getContactJwtFromJwtUrl(jwtInput); + const jwt: string = getContactJwtFromJwtUrl(jwtInput) || ""; const payload = decodeEndorserJwt(jwt).payload; if (Array.isArray(payload.contacts)) { diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index 8fb907fd..95917e12 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -169,11 +169,64 @@ icon-name="check" svg-class="-ml-1 mr-3 h-5 w-5" /> - Migrate All (Recommended) + Migrate All
+ +
+
+
+ +
+
+

Error

+
+

{{ error }}

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

Success

+
+

{{ successMessage }}

+
+
+
+
+ +
+ + + - - @@ -287,49 +331,6 @@ - -
-
-
- -
-
-

Error

-
-

{{ error }}

-
-
-
-
- - -
-
-
- -
-
-

Success

-
-

{{ successMessage }}

-
-
-
-
-
@@ -945,6 +946,7 @@