Browse Source

IndexedDB migration: implement the migrations differently

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

4
src/libs/util.ts

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

414
src/services/indexedDBMigrationService.ts

@ -26,12 +26,11 @@ import "dexie-export-import";
import { PlatformServiceFactory } from "./PlatformServiceFactory";
import { db, accountsDBPromise } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import { Settings, MASTER_SETTINGS_KEY, SettingsWithJsonStrings, BoundingBox } from "../db/tables/settings";
import { Account, AccountEncrypted } from "../db/tables/accounts";
import { Settings, MASTER_SETTINGS_KEY, BoundingBox } from "../db/tables/settings";
import { Account } from "../db/tables/accounts";
import { logger } from "../utils/logger";
import { mapColumnsToValues, parseJsonField } from "../db/databaseUtil";
import { mapColumnsToValues, parseJsonField, generateUpdateStatement, generateInsertStatement } from "../db/databaseUtil";
import { importFromMnemonic } from "../libs/util";
import { IIdentifier } from "@veramo/core";
/**
* Interface for data comparison results between Dexie and SQLite databases
@ -1028,228 +1027,47 @@ export async function migrateSettings(
try {
const dexieSettings = await getDexieSettings();
const platformService = PlatformServiceFactory.getInstance();
// Group settings by DID to handle duplicates
const settingsByDid = new Map<string, {
master?: Settings;
account?: Settings;
}>();
// Organize settings by DID
dexieSettings.forEach(setting => {
const isMasterSetting = setting.id === MASTER_SETTINGS_KEY;
const did = isMasterSetting ? setting.activeDid : setting.accountDid;
if (!did) {
result.warnings.push(`Setting ${setting.id} has no DID, skipping`);
return;
}
if (!settingsByDid.has(did)) {
settingsByDid.set(did, {});
}
const didSettings = settingsByDid.get(did)!;
if (isMasterSetting) {
didSettings.master = setting;
logger.info("[MigrationService] Found master settings", {
did,
id: setting.id,
firstName: setting.firstName,
isRegistered: setting.isRegistered,
profileImageUrl: setting.profileImageUrl,
showShortcutBvc: setting.showShortcutBvc,
searchBoxes: setting.searchBoxes
});
} 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
]
// loop through dexieSettings,
// load the one with the matching accountDid from sqlite,
// and if one doesn't exist then insert it,
// otherwise, update the fields
dexieSettings.forEach(async (setting) => {
const sqliteSettingRaw = await platformService.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[setting.accountDid]
);
if (sqliteSettingRaw?.values?.length) {
// should cover the master settings, were accountDid is null
const sqliteSetting = mapColumnsToValues(sqliteSettingRaw.columns, sqliteSettingRaw.values) as unknown as Settings;
let conditional: string;
let preparams: unknown[];
if (!setting.accountDid) {
conditional = "accountDid is null";
preparams = [];
} else {
logger.info("[MigrationService] Inserting master settings", { did, masterData });
await platformService.dbQuery(
`INSERT INTO settings (
id,
activeDid,
accountDid,
firstName,
isRegistered,
profileImageUrl,
showShortcutBvc,
searchBoxes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
MASTER_SETTINGS_KEY,
masterData.activeDid,
masterData.accountDid,
masterData.firstName,
masterData.isRegistered,
masterData.profileImageUrl,
masterData.showShortcutBvc,
JSON.stringify(masterData.searchBoxes)
]
);
}
result.settingsMigrated++;
conditional = "accountDid = ?";
preparams = [setting.accountDid];
}
// Process account settings
if (didSettings.account) {
const accountData = {
id: 2, // Account settings always use id 2
activeDid: "", // Empty for account settings
accountDid: did,
apiServer: didSettings.account.apiServer || "",
filterFeedByNearby: didSettings.account.filterFeedByNearby || false,
filterFeedByVisible: didSettings.account.filterFeedByVisible || false,
finishedOnboarding: didSettings.account.finishedOnboarding || false,
firstName: didSettings.account.firstName || "",
hideRegisterPromptOnNewContact: didSettings.account.hideRegisterPromptOnNewContact || false,
isRegistered: didSettings.account.isRegistered || false,
lastName: didSettings.account.lastName || "",
profileImageUrl: didSettings.account.profileImageUrl || "",
searchBoxes: didSettings.account.searchBoxes || [],
showShortcutBvc: didSettings.account.showShortcutBvc || false
};
// Check if account setting exists
const existingAccount = await platformService.dbQuery(
"SELECT id FROM settings WHERE id = ? AND accountDid = ? AND activeDid = ''",
[2, did]
);
if (existingAccount?.values?.length) {
logger.info("[MigrationService] Updating account settings", { did, accountData });
await platformService.dbQuery(
`UPDATE settings SET
activeDid = ?,
accountDid = ?,
firstName = ?,
isRegistered = ?,
profileImageUrl = ?,
showShortcutBvc = ?,
searchBoxes = ?
WHERE id = ?`,
[
accountData.activeDid,
accountData.accountDid,
accountData.firstName,
accountData.isRegistered,
accountData.profileImageUrl,
accountData.showShortcutBvc,
JSON.stringify(accountData.searchBoxes),
2
]
const { sql, params } = generateUpdateStatement(
sqliteSetting as unknown as Record<string, unknown>,
"settings",
conditional,
preparams
);
await platformService.dbExec(sql, params);
result.settingsMigrated++;
} else {
logger.info("[MigrationService] Inserting account settings", { did, accountData });
await platformService.dbQuery(
`INSERT INTO settings (
id,
activeDid,
accountDid,
firstName,
isRegistered,
profileImageUrl,
showShortcutBvc,
searchBoxes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
2,
accountData.activeDid,
accountData.accountDid,
accountData.firstName,
accountData.isRegistered,
accountData.profileImageUrl,
accountData.showShortcutBvc,
JSON.stringify(accountData.searchBoxes)
]
// insert new setting
delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case)
const { sql, params } = generateInsertStatement(
setting as unknown as Record<string, unknown>,
"settings"
);
}
await platformService.dbExec(sql, params);
result.settingsMigrated++;
}
logger.info("[MigrationService] Successfully migrated settings for DID", {
did,
masterMigrated: !!didSettings.master,
accountMigrated: !!didSettings.account
});
} catch (error) {
const errorMessage = `Failed to migrate settings for DID ${did}: ${error}`;
result.errors.push(errorMessage);
logger.error("[MigrationService] Settings migration failed:", {
error,
did
});
}
}
if (result.errors.length > 0) {
result.success = false;
}
return result;
} catch (error) {
const errorMessage = `Settings migration failed: ${error}`;
@ -1277,7 +1095,6 @@ export async function migrateSettings(
*
* @async
* @function migrateAccounts
* @param {boolean} [overwriteExisting=false] - Whether to overwrite existing accounts in SQLite
* @returns {Promise<MigrationResult>} Detailed results of the migration operation
* @throws {Error} If the migration process fails completely
* @example
@ -1294,12 +1111,8 @@ export async function migrateSettings(
* }
* ```
*/
export async function migrateAccounts(
overwriteExisting: boolean = false,
): Promise<MigrationResult> {
logger.info("[MigrationService] Starting account migration", {
overwriteExisting,
});
export async function migrateAccounts(): Promise<MigrationResult> {
logger.info("[MigrationService] Starting account migration");
const result: MigrationResult = {
success: true,
@ -1335,67 +1148,17 @@ export async function migrateAccounts(
[did]
);
if (existingResult?.values?.length && !overwriteExisting) {
if (existingResult?.values?.length) {
result.warnings.push(`Account with DID ${did} already exists, skipping`);
continue;
}
// Map Dexie fields to SQLite fields
const accountData = {
did: account.did,
dateCreated: account.dateCreated,
derivationPath: account.derivationPath || "",
identityEncrBase64: account.identity || "",
mnemonicEncrBase64: account.mnemonic || "",
passkeyCredIdHex: account.passkeyCredIdHex || "",
publicKeyHex: account.publicKeyHex || ""
};
// Insert or update the account
if (existingResult?.values?.length) {
await platformService.dbQuery(
`UPDATE accounts SET
dateCreated = ?,
derivationPath = ?,
identityEncrBase64 = ?,
mnemonicEncrBase64 = ?,
passkeyCredIdHex = ?,
publicKeyHex = ?
WHERE did = ?`,
[
accountData.dateCreated,
accountData.derivationPath,
accountData.identityEncrBase64,
accountData.mnemonicEncrBase64,
accountData.passkeyCredIdHex,
accountData.publicKeyHex,
did
]
);
if (account.mnemonic) {
await importFromMnemonic(account.mnemonic, account.derivationPath);
result.accountsMigrated++;
} else {
await platformService.dbQuery(
`INSERT INTO accounts (
did,
dateCreated,
derivationPath,
identityEncrBase64,
mnemonicEncrBase64,
passkeyCredIdHex,
publicKeyHex
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
did,
accountData.dateCreated,
accountData.derivationPath,
accountData.identityEncrBase64,
accountData.mnemonicEncrBase64,
accountData.passkeyCredIdHex,
accountData.publicKeyHex
]
);
result.errors.push(`Account with DID ${did} has no mnemonic, skipping`);
}
result.accountsMigrated++;
logger.info("[MigrationService] Successfully migrated account", {
did,
dateCreated: account.dateCreated
@ -1424,99 +1187,6 @@ export async function migrateAccounts(
}
}
/**
* Generates SQL INSERT statement and parameters from a model object
*
* This helper function creates a parameterized SQL INSERT statement
* from a JavaScript object. It filters out undefined values and
* creates the appropriate SQL syntax with placeholders.
*
* The function is used internally by the migration functions to
* safely insert data into the SQLite database.
*
* @function generateInsertStatement
* @param {Record<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
*
@ -1551,7 +1221,7 @@ export async function migrateAll(
// Step 1: Migrate Accounts (foundational)
logger.info("[MigrationService] Step 1: Migrating accounts...");
const accountsResult = await migrateAccounts(overwriteExisting);
const accountsResult = await migrateAccounts();
if (!accountsResult.success) {
result.errors.push(
`Account migration failed: ${accountsResult.errors.join(", ")}`,

2
src/views/ContactImportView.vue

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

179
src/views/DatabaseMigration.vue

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

2
src/views/ImportAccountView.vue

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

Loading…
Cancel
Save