Browse Source

IndexedDB migration: fix settings update

migrate-dexie-to-sqlite
Trent Larson 6 days ago
parent
commit
8e605d04d7
  1. 93
      src/services/indexedDBMigrationService.ts
  2. 25
      src/views/DatabaseMigration.vue

93
src/services/indexedDBMigrationService.ts

@ -898,6 +898,10 @@ export function generateComparisonYaml(comparison: DataComparison): string {
* } * }
* ``` * ```
*/ */
/**
*
* I recommend using the existing contact import view to migrate contacts.
*
export async function migrateContacts( export async function migrateContacts(
overwriteExisting: boolean = false, overwriteExisting: boolean = false,
): Promise<MigrationResult> { ): Promise<MigrationResult> {
@ -976,6 +980,8 @@ export async function migrateContacts(
return result; return result;
} }
} }
*
*/
/** /**
* Migrates specific settings fields from Dexie to SQLite database * Migrates specific settings fields from Dexie to SQLite database
@ -991,13 +997,12 @@ export async function migrateContacts(
* *
* @async * @async
* @function migrateSettings * @function migrateSettings
* @param {boolean} [overwriteExisting=false] - Whether to overwrite existing settings 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
* ```typescript * ```typescript
* try { * try {
* const result = await migrateSettings(true); // Overwrite existing * const result = await migrateSettings();
* if (result.success) { * if (result.success) {
* console.log(`Successfully migrated ${result.settingsMigrated} settings`); * console.log(`Successfully migrated ${result.settingsMigrated} settings`);
* } else { * } else {
@ -1008,12 +1013,8 @@ export async function migrateContacts(
* } * }
* ``` * ```
*/ */
export async function migrateSettings( export async function migrateSettings(): Promise<MigrationResult> {
overwriteExisting: boolean = false, logger.info("[MigrationService] Starting settings migration");
): Promise<MigrationResult> {
logger.info("[MigrationService] Starting settings migration", {
overwriteExisting,
});
const result: MigrationResult = { const result: MigrationResult = {
success: true, success: true,
@ -1026,19 +1027,34 @@ export async function migrateSettings(
try { try {
const dexieSettings = await getDexieSettings(); const dexieSettings = await getDexieSettings();
logger.info("[MigrationService] Migrating settings", {
dexieSettings: dexieSettings.length,
});
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
// loop through dexieSettings,
// load the one with the matching accountDid from sqlite, // Create an array of promises for all settings migrations
// and if one doesn't exist then insert it, const migrationPromises = dexieSettings.map(async (setting) => {
// otherwise, update the fields logger.info("[MigrationService] Starting to migrate settings", setting);
dexieSettings.forEach(async (setting) => { let sqliteSettingRaw: { columns: string[], values: unknown[][] } | undefined;
const sqliteSettingRaw = await platformService.dbQuery( if (!setting.accountDid) {
"SELECT * FROM settings WHERE accountDid = ?", sqliteSettingRaw = await platformService.dbQuery(
[setting.accountDid] "SELECT * FROM settings WHERE accountDid is null"
); );
} else {
sqliteSettingRaw = await platformService.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[setting.accountDid]
)
}
logger.info("[MigrationService] Migrating one set of settings:", {
setting,
sqliteSettingRaw,
});
if (sqliteSettingRaw?.values?.length) { if (sqliteSettingRaw?.values?.length) {
// should cover the master settings, were accountDid is null // should cover the master settings, were accountDid is null
const sqliteSetting = mapColumnsToValues(sqliteSettingRaw.columns, sqliteSettingRaw.values) as unknown as Settings; const sqliteSettings = mapColumnsToValues(sqliteSettingRaw.columns, sqliteSettingRaw.values) as unknown as Settings[];
const sqliteSetting = sqliteSettings[0];
console.log('sqliteSetting', sqliteSetting)
let conditional: string; let conditional: string;
let preparams: unknown[]; let preparams: unknown[];
if (!setting.accountDid) { if (!setting.accountDid) {
@ -1054,7 +1070,7 @@ export async function migrateSettings(
conditional, conditional,
preparams preparams
); );
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params)
result.settingsMigrated++; result.settingsMigrated++;
} else { } else {
// insert new setting // insert new setting
@ -1068,12 +1084,17 @@ export async function migrateSettings(
} }
}); });
// Wait for all migrations to complete
const updatedSettings = await Promise.all(migrationPromises);
logger.info("[MigrationService] Finished migrating settings", updatedSettings, result);
return result; return result;
} catch (error) { } catch (error) {
logger.error("[MigrationService] Complete settings migration failed:", error);
const errorMessage = `Settings migration failed: ${error}`; const errorMessage = `Settings migration failed: ${error}`;
result.errors.push(errorMessage); result.errors.push(errorMessage);
result.success = false; result.success = false;
logger.error("[MigrationService] Complete settings migration failed:", error);
return result; return result;
} }
} }
@ -1083,7 +1104,7 @@ export async function migrateSettings(
* *
* This function transfers all accounts from the Dexie database to the * This function transfers all accounts from the Dexie database to the
* SQLite database. It handles both new accounts (INSERT) and existing * SQLite database. It handles both new accounts (INSERT) and existing
* accounts (UPDATE) based on the overwriteExisting parameter. * accounts (UPDATE).
* *
* For accounts with mnemonic data, the function uses importFromMnemonic * For accounts with mnemonic data, the function uses importFromMnemonic
* to ensure proper key derivation and identity creation during migration. * to ensure proper key derivation and identity creation during migration.
@ -1100,7 +1121,7 @@ export async function migrateSettings(
* @example * @example
* ```typescript * ```typescript
* try { * try {
* const result = await migrateAccounts(true); // Overwrite existing * const result = await migrateAccounts();
* if (result.success) { * if (result.success) {
* console.log(`Successfully migrated ${result.accountsMigrated} accounts`); * console.log(`Successfully migrated ${result.accountsMigrated} accounts`);
* } else { * } else {
@ -1199,12 +1220,9 @@ export async function migrateAccounts(): Promise<MigrationResult> {
* The migration runs within a transaction to ensure atomicity. If any step fails, * The migration runs within a transaction to ensure atomicity. If any step fails,
* the entire migration is rolled back. * the entire migration is rolled back.
* *
* @param overwriteExisting - Whether to overwrite existing records in SQLite
* @returns Promise<MigrationResult> - Detailed result of the migration operation * @returns Promise<MigrationResult> - Detailed result of the migration operation
*/ */
export async function migrateAll( export async function migrateAll(): Promise<MigrationResult> {
overwriteExisting: boolean = false,
): Promise<MigrationResult> {
const result: MigrationResult = { const result: MigrationResult = {
success: false, success: false,
contactsMigrated: 0, contactsMigrated: 0,
@ -1233,7 +1251,7 @@ export async function migrateAll(
// Step 2: Migrate Settings (depends on accounts) // Step 2: Migrate Settings (depends on accounts)
logger.info("[MigrationService] Step 2: Migrating settings..."); logger.info("[MigrationService] Step 2: Migrating settings...");
const settingsResult = await migrateSettings(overwriteExisting); const settingsResult = await migrateSettings();
if (!settingsResult.success) { if (!settingsResult.success) {
result.errors.push( result.errors.push(
`Settings migration failed: ${settingsResult.errors.join(", ")}`, `Settings migration failed: ${settingsResult.errors.join(", ")}`,
@ -1244,16 +1262,17 @@ export async function migrateAll(
result.warnings.push(...settingsResult.warnings); result.warnings.push(...settingsResult.warnings);
// Step 3: Migrate Contacts (independent, but after accounts for consistency) // Step 3: Migrate Contacts (independent, but after accounts for consistency)
logger.info("[MigrationService] Step 3: Migrating contacts..."); // ... but which is better done through the contact import view
const contactsResult = await migrateContacts(overwriteExisting); // logger.info("[MigrationService] Step 3: Migrating contacts...");
if (!contactsResult.success) { // const contactsResult = await migrateContacts();
result.errors.push( // if (!contactsResult.success) {
`Contact migration failed: ${contactsResult.errors.join(", ")}`, // result.errors.push(
); // `Contact migration failed: ${contactsResult.errors.join(", ")}`,
return result; // );
} // return result;
result.contactsMigrated = contactsResult.contactsMigrated; // }
result.warnings.push(...contactsResult.warnings); // result.contactsMigrated = contactsResult.contactsMigrated;
// result.warnings.push(...contactsResult.warnings);
// All migrations successful // All migrations successful
result.success = true; result.success = true;

25
src/views/DatabaseMigration.vue

@ -19,8 +19,9 @@
</a> </a>
</div> </div>
<!-- Migration Options -->
<!--
<div class="mt-4"> <div class="mt-4">
<!-- Migration Options -->
<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">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4"> <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
@ -52,6 +53,7 @@
</div> </div>
</div> </div>
</div> </div>
-->
<div <div
class="text-lg leading-6 font-medium text-red-900 mt-4" class="text-lg leading-6 font-medium text-red-900 mt-4"
@ -59,7 +61,7 @@
<p <p
v-if="comparison && (comparison.sqliteAccounts.length > 1 || comparison.sqliteSettings.length > 1 || comparison.sqliteContacts.length > 0)" v-if="comparison && (comparison.sqliteAccounts.length > 1 || comparison.sqliteSettings.length > 1 || comparison.sqliteContacts.length > 0)"
> >
Beware: you have unexpected existing data in the SQLite database that will be overwritten. Talk with Trent. Beware: you have unexpected existing data in the new database that will be overwritten. Talk with Trent.
</p> </p>
<p <p
v-if="cannotfindMainAccount" v-if="cannotfindMainAccount"
@ -951,7 +953,6 @@ import { Router } from "vue-router";
import IconRenderer from "../components/IconRenderer.vue"; import IconRenderer from "../components/IconRenderer.vue";
import { import {
compareDatabases, compareDatabases,
migrateContacts,
migrateSettings, migrateSettings,
migrateAccounts, migrateAccounts,
migrateAll, migrateAll,
@ -1004,7 +1005,6 @@ export default class DatabaseMigration extends Vue {
private loadingMessage = ""; private loadingMessage = "";
private error = ""; private error = "";
private exportedData: Record<string, any> | null = null; private exportedData: Record<string, any> | null = null;
private overwriteExisting = true;
private successMessage = ""; private successMessage = "";
useClipboard = useClipboard; useClipboard = useClipboard;
@ -1075,7 +1075,7 @@ export default class DatabaseMigration extends Vue {
window.alert('Copied to clipboard!'); window.alert('Copied to clipboard!');
} }
} catch (error) { } catch (error) {
console.error('Failed to copy to clipboard:', error); logger.error('Failed to copy to clipboard:', error);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.alert('Failed to copy to clipboard'); window.alert('Failed to copy to clipboard');
} }
@ -1141,7 +1141,7 @@ export default class DatabaseMigration extends Vue {
this.clearMessages(); this.clearMessages();
try { try {
const result: MigrationResult = await migrateAll(this.overwriteExisting); const result: MigrationResult = await migrateAll();
if (result.success) { if (result.success) {
const totalMigrated = const totalMigrated =
@ -1209,8 +1209,7 @@ export default class DatabaseMigration extends Vue {
/** /**
* Migrates contacts from Dexie to SQLite database * Migrates contacts from Dexie to SQLite database
* *
* This method transfers contacts from the Dexie database to SQLite, * This method transfers contacts from the Dexie database to SQLite.
* with options to overwrite existing records.
* *
* @async * @async
* @returns {Promise<void>} * @returns {Promise<void>}
@ -1230,8 +1229,7 @@ export default class DatabaseMigration extends Vue {
/** /**
* Migrates settings from Dexie to SQLite database * Migrates settings from Dexie to SQLite database
* *
* This method transfers settings from the Dexie database to SQLite, * This method transfers settings from the Dexie database to SQLite.
* with options to overwrite existing records.
* *
* @async * @async
* @returns {Promise<void>} * @returns {Promise<void>}
@ -1241,9 +1239,7 @@ export default class DatabaseMigration extends Vue {
this.clearMessages(); this.clearMessages();
try { try {
const result: MigrationResult = await migrateSettings( const result: MigrationResult = await migrateSettings();
this.overwriteExisting,
);
if (result.success) { if (result.success) {
this.successMessage = `Successfully migrated ${result.settingsMigrated} settings.`; this.successMessage = `Successfully migrated ${result.settingsMigrated} settings.`;
@ -1276,8 +1272,7 @@ export default class DatabaseMigration extends Vue {
* Migrates accounts from Dexie to SQLite database * Migrates accounts from Dexie to SQLite database
* *
* This method transfers accounts from the Dexie database to SQLite, * This method transfers accounts from the Dexie database to SQLite,
* with options to overwrite existing records. For accounts with mnemonic * For accounts with mnemonic data, it uses the importFromMnemonic utility for proper key derivation.
* data, it uses the importFromMnemonic utility for proper key derivation.
* *
* @async * @async
* @returns {Promise<void>} * @returns {Promise<void>}

Loading…
Cancel
Save