forked from trent_larson/crowd-funder-for-time-pwa
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
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user