forked from trent_larson/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 { 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<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
|
||||
*
|
||||
@@ -346,13 +462,21 @@ export async function getSqliteSettings(): Promise<Settings[]> {
|
||||
export async function compareDatabases(): Promise<DataComparison> {
|
||||
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<DataComparison> {
|
||||
// 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<DataComparison> {
|
||||
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<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
|
||||
*
|
||||
|
||||
@@ -140,6 +140,27 @@
|
||||
Migrate Settings
|
||||
</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
|
||||
: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"
|
||||
@@ -250,7 +271,7 @@
|
||||
<!-- Comparison Results -->
|
||||
<div v-if="comparison" class="space-y-6">
|
||||
<!-- 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="p-5">
|
||||
<div class="flex items-center">
|
||||
@@ -384,10 +405,74 @@
|
||||
</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>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
@@ -601,6 +686,112 @@
|
||||
</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>
|
||||
|
||||
<!-- Migration Options -->
|
||||
@@ -645,6 +836,7 @@ import {
|
||||
compareDatabases,
|
||||
migrateContacts,
|
||||
migrateSettings,
|
||||
migrateAccounts,
|
||||
generateComparisonYaml,
|
||||
type DataComparison,
|
||||
type MigrationResult,
|
||||
@@ -707,7 +899,7 @@ export default class DatabaseMigration extends Vue {
|
||||
|
||||
try {
|
||||
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(
|
||||
"[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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user