Browse Source

IndexedDB migration: implement the migrations differently

migrate-dexie-to-sqlite
Trent Larson 7 days ago
parent
commit
da0b244bae
  1. 56
      src/db/databaseUtil.ts
  2. 4
      src/libs/util.ts
  3. 414
      src/services/indexedDBMigrationService.ts
  4. 2
      src/views/ContactImportView.vue
  5. 179
      src/views/DatabaseMigration.vue
  6. 2
      src/views/ImportAccountView.vue

56
src/db/databaseUtil.ts

@ -227,10 +227,28 @@ export async function logConsoleAndDb(
} }
/** /**
* Generates an SQL INSERT statement and parameters from a model object. * Generates 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 * This helper function creates a parameterized SQL INSERT statement
* @returns Object containing the SQL statement and parameters array * 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<string, unknown>} 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( export function generateInsertStatement(
model: Record<string, unknown>, model: Record<string, unknown>,
@ -248,12 +266,30 @@ export function generateInsertStatement(
} }
/** /**
* Generates an SQL UPDATE statement and parameters from a model object. * Generates 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 * This helper function creates a parameterized SQL UPDATE statement
* @param whereClause The WHERE clause for the update (e.g. "id = ?") * from a JavaScript object. It filters out undefined values and
* @param whereParams Parameters for the WHERE clause * creates the appropriate SQL syntax with placeholders.
* @returns Object containing the SQL statement and parameters array *
* The function is used internally by the migration functions to
* safely update data in the SQLite database.
*
* @function generateUpdateStatement
* @param {Record<string, unknown>} 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( export function generateUpdateStatement(
model: Record<string, unknown>, model: Record<string, unknown>,

4
src/libs/util.ts

@ -1021,12 +1021,12 @@ export async function importFromMnemonic(
// Create new identifier // Create new identifier
const newId = newIdentifier(address, publicHex, privateHex, derivationPath); const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
// Handle database operations // Handle erasures
const accountsDB = await accountsDBPromise;
if (shouldErase) { if (shouldErase) {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec("DELETE FROM accounts"); await platformService.dbExec("DELETE FROM accounts");
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise;
await accountsDB.accounts.clear(); await accountsDB.accounts.clear();
} }
} }

414
src/services/indexedDBMigrationService.ts

@ -26,12 +26,11 @@ import "dexie-export-import";
import { PlatformServiceFactory } from "./PlatformServiceFactory"; import { PlatformServiceFactory } from "./PlatformServiceFactory";
import { db, accountsDBPromise } 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, MASTER_SETTINGS_KEY, SettingsWithJsonStrings, BoundingBox } from "../db/tables/settings"; import { Settings, MASTER_SETTINGS_KEY, BoundingBox } from "../db/tables/settings";
import { Account, AccountEncrypted } from "../db/tables/accounts"; import { Account } from "../db/tables/accounts";
import { logger } from "../utils/logger"; 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 { importFromMnemonic } from "../libs/util";
import { IIdentifier } from "@veramo/core";
/** /**
* Interface for data comparison results between Dexie and SQLite databases * Interface for data comparison results between Dexie and SQLite databases
@ -1028,228 +1027,47 @@ export async function migrateSettings(
try { try {
const dexieSettings = await getDexieSettings(); const dexieSettings = await getDexieSettings();
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
// loop through dexieSettings,
// Group settings by DID to handle duplicates // load the one with the matching accountDid from sqlite,
const settingsByDid = new Map<string, { // and if one doesn't exist then insert it,
master?: Settings; // otherwise, update the fields
account?: Settings; dexieSettings.forEach(async (setting) => {
}>(); const sqliteSettingRaw = await platformService.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
// Organize settings by DID [setting.accountDid]
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
});
} 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
});
}
});
// 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
]
); );
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 { } else {
logger.info("[MigrationService] Inserting master settings", { did, masterData }); conditional = "accountDid = ?";
await platformService.dbQuery( preparams = [setting.accountDid];
`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++;
} }
const { sql, params } = generateUpdateStatement(
// Process account settings sqliteSetting as unknown as Record<string, unknown>,
if (didSettings.account) { "settings",
const accountData = { conditional,
id: 2, // Account settings always use id 2 preparams
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
]
); );
await platformService.dbExec(sql, params);
result.settingsMigrated++;
} else { } else {
logger.info("[MigrationService] Inserting account settings", { did, accountData }); // insert new setting
await platformService.dbQuery( delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case)
`INSERT INTO settings ( const { sql, params } = generateInsertStatement(
id, setting as unknown as Record<string, unknown>,
activeDid, "settings"
accountDid,
firstName,
isRegistered,
profileImageUrl,
showShortcutBvc,
searchBoxes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
2,
accountData.activeDid,
accountData.accountDid,
accountData.firstName,
accountData.isRegistered,
accountData.profileImageUrl,
accountData.showShortcutBvc,
JSON.stringify(accountData.searchBoxes)
]
); );
} await platformService.dbExec(sql, params);
result.settingsMigrated++; 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; return result;
} catch (error) { } catch (error) {
const errorMessage = `Settings migration failed: ${error}`; const errorMessage = `Settings migration failed: ${error}`;
@ -1277,7 +1095,6 @@ export async function migrateSettings(
* *
* @async * @async
* @function migrateAccounts * @function migrateAccounts
* @param {boolean} [overwriteExisting=false] - Whether to overwrite existing accounts in SQLite
* @returns {Promise<MigrationResult>} Detailed results of the migration operation * @returns {Promise<MigrationResult>} Detailed results of the migration operation
* @throws {Error} If the migration process fails completely * @throws {Error} If the migration process fails completely
* @example * @example
@ -1294,12 +1111,8 @@ export async function migrateSettings(
* } * }
* ``` * ```
*/ */
export async function migrateAccounts( export async function migrateAccounts(): Promise<MigrationResult> {
overwriteExisting: boolean = false, logger.info("[MigrationService] Starting account migration");
): Promise<MigrationResult> {
logger.info("[MigrationService] Starting account migration", {
overwriteExisting,
});
const result: MigrationResult = { const result: MigrationResult = {
success: true, success: true,
@ -1335,67 +1148,17 @@ export async function migrateAccounts(
[did] [did]
); );
if (existingResult?.values?.length && !overwriteExisting) { if (existingResult?.values?.length) {
result.warnings.push(`Account with DID ${did} already exists, skipping`); result.warnings.push(`Account with DID ${did} already exists, skipping`);
continue; continue;
} }
if (account.mnemonic) {
// Map Dexie fields to SQLite fields await importFromMnemonic(account.mnemonic, account.derivationPath);
const accountData = { result.accountsMigrated++;
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
]
);
} else { } else {
await platformService.dbQuery( result.errors.push(`Account with DID ${did} has no mnemonic, skipping`);
`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.accountsMigrated++;
logger.info("[MigrationService] Successfully migrated account", { logger.info("[MigrationService] Successfully migrated account", {
did, did,
dateCreated: account.dateCreated 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<string, unknown>} 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<string, unknown>,
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<string, unknown>} 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<string, unknown>,
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 * Migrates all data from Dexie to SQLite in the proper order
* *
@ -1551,7 +1221,7 @@ export async function migrateAll(
// Step 1: Migrate Accounts (foundational) // Step 1: Migrate Accounts (foundational)
logger.info("[MigrationService] Step 1: Migrating accounts..."); logger.info("[MigrationService] Step 1: Migrating accounts...");
const accountsResult = await migrateAccounts(overwriteExisting); const accountsResult = await migrateAccounts();
if (!accountsResult.success) { if (!accountsResult.success) {
result.errors.push( result.errors.push(
`Account migration failed: ${accountsResult.errors.join(", ")}`, `Account migration failed: ${accountsResult.errors.join(", ")}`,

2
src/views/ContactImportView.vue

@ -566,7 +566,7 @@ export default class ContactImportView extends Vue {
this.checkingImports = true; this.checkingImports = true;
try { try {
const jwt: string = getContactJwtFromJwtUrl(jwtInput); const jwt: string = getContactJwtFromJwtUrl(jwtInput) || "";
const payload = decodeEndorserJwt(jwt).payload; const payload = decodeEndorserJwt(jwt).payload;
if (Array.isArray(payload.contacts)) { if (Array.isArray(payload.contacts)) {

179
src/views/DatabaseMigration.vue

@ -169,11 +169,64 @@
icon-name="check" icon-name="check"
svg-class="-ml-1 mr-3 h-5 w-5" svg-class="-ml-1 mr-3 h-5 w-5"
/> />
Migrate All (Recommended) Migrate All
</button> </button>
<div class="w-full border-t border-gray-200 my-4"></div> <div class="w-full border-t border-gray-200 my-4"></div>
<!-- Error State -->
<div
v-if="error"
class="mb-6 bg-red-50 border border-red-200 rounded-lg p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<IconRenderer
icon-name="warning"
svg-class="h-5 w-5 text-red-400"
fill="currentColor"
/>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Error</h3>
<div class="mt-2 text-sm text-red-700">
<p>{{ error }}</p>
</div>
</div>
</div>
</div>
<!-- Success State -->
<div
v-if="successMessage"
class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<IconRenderer
icon-name="check"
svg-class="h-5 w-5 text-green-400"
/>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-green-800">Success</h3>
<div class="mt-2 text-sm text-green-700">
<p>{{ successMessage }}</p>
</div>
</div>
</div>
</div>
<div class="w-full border-t border-gray-200 my-4"></div>
<button
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"
>
<IconRenderer icon-name="download" svg-class="-ml-1 mr-3 h-5 w-5" />
Export Comparison
</button>
<button <button
:disabled="isLoading" :disabled="isLoading"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 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-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
@ -194,16 +247,16 @@
</button> </button>
<button <button
:disabled="isLoading || !downloadSettingsContactsBlob || !comparison" :disabled="isLoading"
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" 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="migrateContacts" @click="migrateAccounts"
> >
<IconRenderer icon-name="plus" svg-class="-ml-1 mr-3 h-5 w-5" /> <IconRenderer icon-name="lock" svg-class="-ml-1 mr-3 h-5 w-5" />
Migrate Contacts Migrate Accounts
</button> </button>
<button <button
:disabled="isLoading || !downloadSettingsContactsBlob || !comparison" :disabled="isLoading"
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" 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" @click="migrateSettings"
> >
@ -212,21 +265,12 @@
</button> </button>
<button <button
:disabled="isLoading || !downloadMnemonic || !comparison" :disabled="isLoading"
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" 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="migrateAccounts" @click="migrateContacts"
>
<IconRenderer icon-name="lock" svg-class="-ml-1 mr-3 h-5 w-5" />
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"
@click="exportComparison"
> >
<IconRenderer icon-name="download" svg-class="-ml-1 mr-3 h-5 w-5" /> <IconRenderer icon-name="plus" svg-class="-ml-1 mr-3 h-5 w-5" />
Export Comparison Migrate Contacts
</button> </button>
</div> </div>
@ -287,49 +331,6 @@
</div> </div>
</div> </div>
<!-- Error State -->
<div
v-if="error"
class="mb-6 bg-red-50 border border-red-200 rounded-lg p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<IconRenderer
icon-name="warning"
svg-class="h-5 w-5 text-red-400"
fill="currentColor"
/>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">Error</h3>
<div class="mt-2 text-sm text-red-700">
<p>{{ error }}</p>
</div>
</div>
</div>
</div>
<!-- Success State -->
<div
v-if="successMessage"
class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<IconRenderer
icon-name="check"
svg-class="h-5 w-5 text-green-400"
/>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-green-800">Success</h3>
<div class="mt-2 text-sm text-green-700">
<p>{{ successMessage }}</p>
</div>
</div>
</div>
</div>
<!-- Comparison Results --> <!-- Comparison Results -->
<div v-if="comparison" class="space-y-6"> <div v-if="comparison" class="space-y-6">
<!-- Summary Cards --> <!-- Summary Cards -->
@ -945,6 +946,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { Router } from "vue-router";
import IconRenderer from "../components/IconRenderer.vue"; import IconRenderer from "../components/IconRenderer.vue";
import { import {
@ -989,6 +991,8 @@ import { logger } from "../utils/logger";
}, },
}) })
export default class DatabaseMigration extends Vue { export default class DatabaseMigration extends Vue {
$router!: Router;
// Component state // Component state
private comparison: DataComparison | null = null; private comparison: DataComparison | null = null;
private cannotfindMainAccount = false; private cannotfindMainAccount = false;
@ -1148,6 +1152,7 @@ export default class DatabaseMigration extends Vue {
if (result.warnings.length > 0) { if (result.warnings.length > 0) {
this.successMessage += ` ${result.warnings.length} warnings.`; this.successMessage += ` ${result.warnings.length} warnings.`;
} }
this.successMessage += " Now finish by migrating contacts.";
logger.info( logger.info(
"[DatabaseMigration] Complete migration successful", "[DatabaseMigration] Complete migration successful",
result, result,
@ -1211,39 +1216,15 @@ export default class DatabaseMigration extends Vue {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async migrateContacts(): Promise<void> { async migrateContacts(): Promise<void> {
this.setLoading("Migrating contacts..."); // load all contacts from indexedDB
this.clearMessages(); const dexieContacts = await getDexieContacts();
// now reroute to the contact import view with query parameter of contacts
try { this.$router.push({
const result: MigrationResult = await migrateContacts( name: "contact-import",
this.overwriteExisting, query: {
); contacts: JSON.stringify(dexieContacts),
},
if (result.success) { });
this.successMessage = `Successfully migrated ${result.contactsMigrated} contacts.`;
if (result.warnings.length > 0) {
this.successMessage += ` ${result.warnings.length} warnings.`;
}
logger.info(
"[DatabaseMigration] Contact migration completed successfully",
result,
);
// Refresh comparison data after successful migration
this.comparison = await compareDatabases();
} else {
this.error = `Migration failed: ${result.errors.join(", ")}`;
logger.error(
"[DatabaseMigration] Contact migration failed:",
result.errors,
);
}
} catch (error) {
this.error = `Failed to migrate contacts: ${error}`;
logger.error("[DatabaseMigration] Contact migration failed:", error);
} finally {
this.setLoading("");
}
} }
/** /**
@ -1306,9 +1287,7 @@ export default class DatabaseMigration extends Vue {
this.clearMessages(); this.clearMessages();
try { try {
const result: MigrationResult = await migrateAccounts( const result: MigrationResult = await migrateAccounts();
this.overwriteExisting,
);
if (result.success) { if (result.success) {
this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`; this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`;

2
src/views/ImportAccountView.vue

@ -51,7 +51,7 @@
<div v-if="numAccounts == 1" class="mt-4"> <div v-if="numAccounts == 1" class="mt-4">
<input v-model="shouldErase" type="checkbox" class="mr-2" /> <input v-model="shouldErase" type="checkbox" class="mr-2" />
<label>Erase the previous identifier.</label> <label>Erase previous identifiers.</label>
</div> </div>
<div v-if="isNotProdServer()" class="mt-4 text-blue-500"> <div v-if="isNotProdServer()" class="mt-4 text-blue-500">

Loading…
Cancel
Save