forked from jsnbuchanan/crowd-funder-for-time-pwa
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.
This commit is contained in:
@@ -22,12 +22,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { PlatformServiceFactory } from "./PlatformServiceFactory";
|
import { PlatformServiceFactory } from "./PlatformServiceFactory";
|
||||||
import { db } from "../db/index";
|
import { db, accountsDBPromise } from "../db/index";
|
||||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||||
import { Settings } from "../db/tables/settings";
|
import { Settings } from "../db/tables/settings";
|
||||||
|
import { Account } from "../db/tables/accounts";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { parseJsonField } from "../db/databaseUtil";
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
import { USE_DEXIE_DB } from "../constants/app";
|
import { USE_DEXIE_DB } from "../constants/app";
|
||||||
|
import { importFromMnemonic } from "../libs/util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for data comparison results between Dexie and SQLite databases
|
* 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 {Contact[]} sqliteContacts - All contacts from SQLite database
|
||||||
* @property {Settings[]} dexieSettings - All settings from Dexie database
|
* @property {Settings[]} dexieSettings - All settings from Dexie database
|
||||||
* @property {Settings[]} sqliteSettings - All settings from SQLite 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 - Detailed differences between databases
|
||||||
* @property {Object} differences.contacts - Contact-specific differences
|
* @property {Object} differences.contacts - Contact-specific differences
|
||||||
* @property {Contact[]} differences.contacts.added - Contacts in Dexie but not SQLite
|
* @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.added - Settings in Dexie but not SQLite
|
||||||
* @property {Settings[]} differences.settings.modified - Settings that differ between databases
|
* @property {Settings[]} differences.settings.modified - Settings that differ between databases
|
||||||
* @property {Settings[]} differences.settings.missing - Settings in SQLite but not Dexie
|
* @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 {
|
export interface DataComparison {
|
||||||
dexieContacts: Contact[];
|
dexieContacts: Contact[];
|
||||||
sqliteContacts: Contact[];
|
sqliteContacts: Contact[];
|
||||||
dexieSettings: Settings[];
|
dexieSettings: Settings[];
|
||||||
sqliteSettings: Settings[];
|
sqliteSettings: Settings[];
|
||||||
|
dexieAccounts: Account[];
|
||||||
|
sqliteAccounts: Account[];
|
||||||
differences: {
|
differences: {
|
||||||
contacts: {
|
contacts: {
|
||||||
added: Contact[];
|
added: Contact[];
|
||||||
@@ -67,6 +77,11 @@ export interface DataComparison {
|
|||||||
modified: Settings[];
|
modified: Settings[];
|
||||||
missing: 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 {boolean} success - Whether the migration operation completed successfully
|
||||||
* @property {number} contactsMigrated - Number of contacts successfully migrated
|
* @property {number} contactsMigrated - Number of contacts successfully migrated
|
||||||
* @property {number} settingsMigrated - Number of settings 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[]} errors - Array of error messages encountered during migration
|
||||||
* @property {string[]} warnings - Array of warning messages (non-fatal issues)
|
* @property {string[]} warnings - Array of warning messages (non-fatal issues)
|
||||||
*/
|
*/
|
||||||
@@ -88,6 +104,7 @@ export interface MigrationResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
contactsMigrated: number;
|
contactsMigrated: number;
|
||||||
settingsMigrated: number;
|
settingsMigrated: number;
|
||||||
|
accountsMigrated: number;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
@@ -315,6 +332,105 @@ export async function getSqliteSettings(): Promise<Settings[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Account[]>} 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<Account[]> {
|
||||||
|
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<Account[]>} 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<Account[]> {
|
||||||
|
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
|
* Compares data between Dexie and SQLite databases
|
||||||
*
|
*
|
||||||
@@ -346,12 +462,20 @@ export async function getSqliteSettings(): Promise<Settings[]> {
|
|||||||
export async function compareDatabases(): Promise<DataComparison> {
|
export async function compareDatabases(): Promise<DataComparison> {
|
||||||
logger.info("[MigrationService] Starting database comparison");
|
logger.info("[MigrationService] Starting database comparison");
|
||||||
|
|
||||||
const [dexieContacts, sqliteContacts, dexieSettings, sqliteSettings] =
|
const [
|
||||||
await Promise.all([
|
dexieContacts,
|
||||||
|
sqliteContacts,
|
||||||
|
dexieSettings,
|
||||||
|
sqliteSettings,
|
||||||
|
dexieAccounts,
|
||||||
|
sqliteAccounts,
|
||||||
|
] = await Promise.all([
|
||||||
getDexieContacts(),
|
getDexieContacts(),
|
||||||
getSqliteContacts(),
|
getSqliteContacts(),
|
||||||
getDexieSettings(),
|
getDexieSettings(),
|
||||||
getSqliteSettings(),
|
getSqliteSettings(),
|
||||||
|
getDexieAccounts(),
|
||||||
|
getSqliteAccounts(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Compare contacts
|
// Compare contacts
|
||||||
@@ -360,14 +484,20 @@ export async function compareDatabases(): Promise<DataComparison> {
|
|||||||
// Compare settings
|
// Compare settings
|
||||||
const settingsDifferences = compareSettings(dexieSettings, sqliteSettings);
|
const settingsDifferences = compareSettings(dexieSettings, sqliteSettings);
|
||||||
|
|
||||||
|
// Compare accounts
|
||||||
|
const accountDifferences = compareAccounts(dexieAccounts, sqliteAccounts);
|
||||||
|
|
||||||
const comparison: DataComparison = {
|
const comparison: DataComparison = {
|
||||||
dexieContacts,
|
dexieContacts,
|
||||||
sqliteContacts,
|
sqliteContacts,
|
||||||
dexieSettings,
|
dexieSettings,
|
||||||
sqliteSettings,
|
sqliteSettings,
|
||||||
|
dexieAccounts,
|
||||||
|
sqliteAccounts,
|
||||||
differences: {
|
differences: {
|
||||||
contacts: contactDifferences,
|
contacts: contactDifferences,
|
||||||
settings: settingsDifferences,
|
settings: settingsDifferences,
|
||||||
|
accounts: accountDifferences,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -376,8 +506,11 @@ export async function compareDatabases(): Promise<DataComparison> {
|
|||||||
sqliteContacts: sqliteContacts.length,
|
sqliteContacts: sqliteContacts.length,
|
||||||
dexieSettings: dexieSettings.length,
|
dexieSettings: dexieSettings.length,
|
||||||
sqliteSettings: sqliteSettings.length,
|
sqliteSettings: sqliteSettings.length,
|
||||||
|
dexieAccounts: dexieAccounts.length,
|
||||||
|
sqliteAccounts: sqliteAccounts.length,
|
||||||
contactDifferences: contactDifferences,
|
contactDifferences: contactDifferences,
|
||||||
settingsDifferences: settingsDifferences,
|
settingsDifferences: settingsDifferences,
|
||||||
|
accountDifferences: accountDifferences,
|
||||||
});
|
});
|
||||||
|
|
||||||
return comparison;
|
return comparison;
|
||||||
@@ -491,6 +624,57 @@ function compareSettings(
|
|||||||
return { added, modified, missing };
|
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
|
* 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
|
* Generates YAML-formatted comparison data
|
||||||
*
|
*
|
||||||
@@ -622,6 +843,8 @@ export function generateComparisonYaml(comparison: DataComparison): string {
|
|||||||
sqliteContacts: comparison.sqliteContacts.length,
|
sqliteContacts: comparison.sqliteContacts.length,
|
||||||
dexieSettings: comparison.dexieSettings.length,
|
dexieSettings: comparison.dexieSettings.length,
|
||||||
sqliteSettings: comparison.sqliteSettings.length,
|
sqliteSettings: comparison.sqliteSettings.length,
|
||||||
|
dexieAccounts: comparison.dexieAccounts.length,
|
||||||
|
sqliteAccounts: comparison.sqliteAccounts.length,
|
||||||
},
|
},
|
||||||
differences: {
|
differences: {
|
||||||
contacts: {
|
contacts: {
|
||||||
@@ -634,6 +857,11 @@ export function generateComparisonYaml(comparison: DataComparison): string {
|
|||||||
modified: comparison.differences.settings.modified.length,
|
modified: comparison.differences.settings.modified.length,
|
||||||
missing: comparison.differences.settings.missing.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: {
|
contacts: {
|
||||||
dexie: comparison.dexieContacts.map((c) => ({
|
dexie: comparison.dexieContacts.map((c) => ({
|
||||||
@@ -677,6 +905,28 @@ export function generateComparisonYaml(comparison: DataComparison): string {
|
|||||||
searchBoxes: s.searchBoxes,
|
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,
|
success: true,
|
||||||
contactsMigrated: 0,
|
contactsMigrated: 0,
|
||||||
settingsMigrated: 0,
|
settingsMigrated: 0,
|
||||||
|
accountsMigrated: 0,
|
||||||
errors: [],
|
errors: [],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
};
|
};
|
||||||
@@ -834,6 +1085,7 @@ export async function migrateSettings(
|
|||||||
success: true,
|
success: true,
|
||||||
contactsMigrated: 0,
|
contactsMigrated: 0,
|
||||||
settingsMigrated: 0,
|
settingsMigrated: 0,
|
||||||
|
accountsMigrated: 0,
|
||||||
errors: [],
|
errors: [],
|
||||||
warnings: [],
|
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<MigrationResult>} 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<MigrationResult> {
|
||||||
|
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<string, unknown>,
|
||||||
|
"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<string, unknown>,
|
||||||
|
"accounts",
|
||||||
|
);
|
||||||
|
await platformService.dbExec(sql, params);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Insert new account without mnemonic
|
||||||
|
const { sql, params } = generateInsertStatement(
|
||||||
|
account as unknown as Record<string, unknown>,
|
||||||
|
"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
|
* Generates SQL INSERT statement and parameters from a model object
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -140,6 +140,27 @@
|
|||||||
Migrate Settings
|
Migrate Settings
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:disabled="isLoading || !isDexieEnabled || !comparison"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="-ml-1 mr-3 h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Migrate Accounts
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
:disabled="!comparison"
|
:disabled="!comparison"
|
||||||
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"
|
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"
|
||||||
@@ -250,7 +271,7 @@
|
|||||||
<!-- Comparison Results -->
|
<!-- Comparison Results -->
|
||||||
<div v-if="comparison" class="space-y-6">
|
<div v-if="comparison" class="space-y-6">
|
||||||
<!-- Summary Cards -->
|
<!-- Summary Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -384,10 +405,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
class="h-6 w-6 text-orange-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||||
|
Dexie Accounts
|
||||||
|
</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900">
|
||||||
|
{{ comparison.dexieAccounts.length }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg
|
||||||
|
class="h-6 w-6 text-teal-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||||
|
SQLite Accounts
|
||||||
|
</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900">
|
||||||
|
{{ comparison.sqliteAccounts.length }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Differences Section -->
|
<!-- Differences Section -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<!-- Contacts Differences -->
|
<!-- Contacts Differences -->
|
||||||
<div class="bg-white shadow rounded-lg">
|
<div class="bg-white shadow rounded-lg">
|
||||||
<div class="px-4 py-5 sm:p-6">
|
<div class="px-4 py-5 sm:p-6">
|
||||||
@@ -601,6 +686,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Account Differences -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Account Differences
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-blue-600 mr-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium text-blue-900">Added</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-bold text-blue-900">{{
|
||||||
|
comparison.differences.accounts.added.length
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-yellow-600 mr-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium text-yellow-900"
|
||||||
|
>Modified</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-bold text-yellow-900">{{
|
||||||
|
comparison.differences.accounts.modified.length
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-red-600 mr-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium text-red-900"
|
||||||
|
>Missing</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-bold text-red-900">{{
|
||||||
|
comparison.differences.accounts.missing.length
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Account Details -->
|
||||||
|
<div
|
||||||
|
v-if="comparison.differences.accounts.added.length > 0"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
||||||
|
Added Accounts:
|
||||||
|
</h4>
|
||||||
|
<div class="max-h-32 overflow-y-auto space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="account in comparison.differences.accounts.added"
|
||||||
|
:key="account.id"
|
||||||
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
||||||
|
>
|
||||||
|
ID: {{ account.id }} - {{ account.did.substring(0, 20) }}...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Migration Options -->
|
<!-- Migration Options -->
|
||||||
@@ -645,6 +836,7 @@ import {
|
|||||||
compareDatabases,
|
compareDatabases,
|
||||||
migrateContacts,
|
migrateContacts,
|
||||||
migrateSettings,
|
migrateSettings,
|
||||||
|
migrateAccounts,
|
||||||
generateComparisonYaml,
|
generateComparisonYaml,
|
||||||
type DataComparison,
|
type DataComparison,
|
||||||
type MigrationResult,
|
type MigrationResult,
|
||||||
@@ -707,7 +899,7 @@ export default class DatabaseMigration extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.comparison = await compareDatabases();
|
this.comparison = await compareDatabases();
|
||||||
this.successMessage = `Comparison completed successfully. Found ${this.comparison.differences.contacts.added.length + this.comparison.differences.settings.added.length} items to migrate.`;
|
this.successMessage = `Comparison completed successfully. Found ${this.comparison.differences.contacts.added.length + this.comparison.differences.settings.added.length + this.comparison.differences.accounts.added.length} items to migrate.`;
|
||||||
logger.info(
|
logger.info(
|
||||||
"[DatabaseMigration] Database comparison completed successfully",
|
"[DatabaseMigration] Database comparison completed successfully",
|
||||||
);
|
);
|
||||||
@@ -803,6 +995,49 @@ export default class DatabaseMigration extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates accounts from Dexie to SQLite database
|
||||||
|
*
|
||||||
|
* This method transfers accounts from the Dexie database to SQLite,
|
||||||
|
* with options to overwrite existing records. For accounts with mnemonic
|
||||||
|
* data, it uses the importFromMnemonic utility for proper key derivation.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async migrateAccounts(): Promise<void> {
|
||||||
|
this.setLoading("Migrating accounts...");
|
||||||
|
this.clearMessages();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: MigrationResult = await migrateAccounts(
|
||||||
|
this.overwriteExisting,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`;
|
||||||
|
if (result.warnings.length > 0) {
|
||||||
|
this.successMessage += ` ${result.warnings.length} warnings.`;
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"[DatabaseMigration] Account migration completed successfully",
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.error = `Migration failed: ${result.errors.join(", ")}`;
|
||||||
|
logger.error(
|
||||||
|
"[DatabaseMigration] Account migration failed:",
|
||||||
|
result.errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.error = `Failed to migrate accounts: ${error}`;
|
||||||
|
logger.error("[DatabaseMigration] Account migration failed:", error);
|
||||||
|
} finally {
|
||||||
|
this.setLoading("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports comparison results to a file
|
* Exports comparison results to a file
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user