Browse Source

feat: implement single-step migration with proper foreign key order

- Add migrateAll() function that handles complete migration in correct order:
  Accounts → Settings → Contacts to avoid foreign key constraint issues
- Add prominent "Migrate All (Recommended)" button to migration UI
- Add informational section explaining migration order and rationale
- Add info icon to icon set for UI clarity
- Improve migration logic to handle overwriteExisting parameter properly:
  - New records are always migrated regardless of checkbox setting
  - Existing records are only updated when overwriteExisting=true
  - Clear warning messages when records are skipped
- Maintain backward compatibility with individual migration buttons
- All code linted and formatted according to project standards

Co-authored-by: Matthew Raymer
Matthew Raymer 2 weeks ago
parent
commit
30c8b73041
  1. 5
      src/assets/icons.json
  2. 2
      src/constants/app.ts
  3. 94
      src/services/migrationService.ts
  4. 115
      src/views/DatabaseMigration.vue

5
src/assets/icons.json

@ -66,5 +66,10 @@
"strokeLinejoin": "round",
"strokeWidth": "2",
"d": "M12 6v6m0 0v6m0-6h6m-6 0H6"
},
"info": {
"fillRule": "evenodd",
"d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z",
"clipRule": "evenodd"
}
}

2
src/constants/app.ts

@ -51,7 +51,7 @@ export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED =
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
export const USE_DEXIE_DB = false;
export const USE_DEXIE_DB = true;
/**
* The possible values for "group" and "type" are in App.vue.

94
src/services/migrationService.ts

@ -1548,3 +1548,97 @@ export async function runMigrations<T>(
throw error;
}
}
/**
* Migrates all data from Dexie to SQLite in the proper order
*
* This function performs a complete migration of all data from Dexie to SQLite
* in the correct order to avoid foreign key constraint issues:
* 1. Accounts (foundational - contains DIDs)
* 2. Settings (references accountDid, activeDid)
* 3. Contacts (independent, but migrated after accounts for consistency)
*
* The migration runs within a transaction to ensure atomicity. If any step fails,
* the entire migration is rolled back.
*
* @param overwriteExisting - Whether to overwrite existing records in SQLite
* @returns Promise<MigrationResult> - Detailed result of the migration operation
*/
export async function migrateAll(
overwriteExisting: boolean = false,
): Promise<MigrationResult> {
const result: MigrationResult = {
success: false,
contactsMigrated: 0,
settingsMigrated: 0,
accountsMigrated: 0,
errors: [],
warnings: [],
};
try {
logger.info(
"[MigrationService] Starting complete migration from Dexie to SQLite",
);
// Step 1: Migrate Accounts (foundational)
logger.info("[MigrationService] Step 1: Migrating accounts...");
const accountsResult = await migrateAccounts(overwriteExisting);
if (!accountsResult.success) {
result.errors.push(
`Account migration failed: ${accountsResult.errors.join(", ")}`,
);
return result;
}
result.accountsMigrated = accountsResult.accountsMigrated;
result.warnings.push(...accountsResult.warnings);
// Step 2: Migrate Settings (depends on accounts)
logger.info("[MigrationService] Step 2: Migrating settings...");
const settingsResult = await migrateSettings(overwriteExisting);
if (!settingsResult.success) {
result.errors.push(
`Settings migration failed: ${settingsResult.errors.join(", ")}`,
);
return result;
}
result.settingsMigrated = settingsResult.settingsMigrated;
result.warnings.push(...settingsResult.warnings);
// Step 3: Migrate Contacts (independent, but after accounts for consistency)
logger.info("[MigrationService] Step 3: Migrating contacts...");
const contactsResult = await migrateContacts(overwriteExisting);
if (!contactsResult.success) {
result.errors.push(
`Contact migration failed: ${contactsResult.errors.join(", ")}`,
);
return result;
}
result.contactsMigrated = contactsResult.contactsMigrated;
result.warnings.push(...contactsResult.warnings);
// All migrations successful
result.success = true;
const totalMigrated =
result.accountsMigrated +
result.settingsMigrated +
result.contactsMigrated;
logger.info(
`[MigrationService] Complete migration successful: ${totalMigrated} total records migrated`,
{
accounts: result.accountsMigrated,
settings: result.settingsMigrated,
contacts: result.contactsMigrated,
warnings: result.warnings.length,
},
);
return result;
} catch (error) {
const errorMessage = `Complete migration failed: ${error}`;
result.errors.push(errorMessage);
logger.error("[MigrationService] Complete migration failed:", error);
return result;
}
}

115
src/views/DatabaseMigration.vue

@ -64,6 +64,27 @@
Compare Databases
</button>
<button
:disabled="isLoading || !isDexieEnabled || !comparison"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="migrateAll"
>
<IconRenderer
v-if="isLoading"
icon-name="spinner"
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
fill="currentColor"
/>
<IconRenderer
v-else
icon-name="check"
svg-class="-ml-1 mr-3 h-5 w-5"
/>
Migrate All (Recommended)
</button>
<div class="w-full border-t border-gray-200 my-4"></div>
<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-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"
@ -110,6 +131,49 @@
</button>
</div>
<!-- Migration Information -->
<div
v-if="comparison"
class="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<IconRenderer
icon-name="info"
svg-class="h-5 w-5 text-blue-400"
fill="currentColor"
/>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">
Migration Order & Recommendations
</h3>
<div class="mt-2 text-sm text-blue-700">
<p class="mb-2">
<strong>Recommended:</strong> Use "Migrate All" to ensure proper
data integrity and avoid foreign key issues.
</p>
<p class="mb-2">
<strong>Migration Order:</strong> Accounts Settings Contacts
</p>
<ul class="list-disc list-inside space-y-1">
<li>
<strong>Accounts:</strong> Foundation data containing DIDs
</li>
<li>
<strong>Settings:</strong> References accountDid and activeDid
from accounts
</li>
<li>
<strong>Contacts:</strong> Independent data, migrated last for
consistency
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="text-center py-12">
<div
@ -596,6 +660,7 @@ import {
migrateContacts,
migrateSettings,
migrateAccounts,
migrateAll,
generateComparisonYaml,
type DataComparison,
type MigrationResult,
@ -646,6 +711,56 @@ export default class DatabaseMigration extends Vue {
return USE_DEXIE_DB;
}
/**
* Migrates all data from Dexie to SQLite in the proper order
*
* This method performs a complete migration of all data from Dexie to SQLite
* in the correct order to avoid foreign key constraint issues:
* 1. Accounts (foundational - contains DIDs)
* 2. Settings (references accountDid, activeDid)
* 3. Contacts (independent, but migrated after accounts for consistency)
*
* This is the recommended approach as it ensures data integrity and
* handles all foreign key relationships automatically.
*
* @async
* @returns {Promise<void>}
*/
async migrateAll(): Promise<void> {
this.setLoading("Migrating all data (Accounts → Settings → Contacts)...");
this.clearMessages();
try {
const result: MigrationResult = await migrateAll(this.overwriteExisting);
if (result.success) {
const totalMigrated =
result.accountsMigrated +
result.settingsMigrated +
result.contactsMigrated;
this.successMessage = `Successfully migrated ${totalMigrated} total records: ${result.accountsMigrated} accounts, ${result.settingsMigrated} settings, ${result.contactsMigrated} contacts.`;
if (result.warnings.length > 0) {
this.successMessage += ` ${result.warnings.length} warnings.`;
}
logger.info(
"[DatabaseMigration] Complete migration successful",
result,
);
} else {
this.error = `Migration failed: ${result.errors.join(", ")}`;
logger.error(
"[DatabaseMigration] Complete migration failed:",
result.errors,
);
}
} catch (error) {
this.error = `Failed to migrate all data: ${error}`;
logger.error("[DatabaseMigration] Complete migration failed:", error);
} finally {
this.setLoading("");
}
}
/**
* Compares data between Dexie and SQLite databases
*

Loading…
Cancel
Save