You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
918 lines
32 KiB
918 lines
32 KiB
<template>
|
|
<div class="min-h-screen bg-gray-50 py-8">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-gray-900">Database Migration</h1>
|
|
<p class="mt-2 text-gray-600">
|
|
Compare and migrate data between Dexie (IndexedDB) and SQLite
|
|
databases
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Status Banner -->
|
|
<div
|
|
v-if="!isDexieEnabled"
|
|
class="mb-6 bg-yellow-50 border border-yellow-200 rounded-lg p-4"
|
|
>
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="warning"
|
|
svg-class="h-5 w-5 text-yellow-400"
|
|
fill="currentColor"
|
|
/>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-yellow-800">
|
|
Dexie Database Disabled
|
|
</h3>
|
|
<div class="mt-2 text-sm text-yellow-700">
|
|
<p>
|
|
To use migration features, enable Dexie database by setting
|
|
<code class="bg-yellow-100 px-1 rounded">
|
|
USE_DEXIE_DB = true
|
|
</code>
|
|
in
|
|
<code class="bg-yellow-100 px-1 rounded">
|
|
constants/app.ts
|
|
</code>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="mb-8 flex flex-wrap gap-4">
|
|
<button
|
|
:disabled="isLoading || !isDexieEnabled"
|
|
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"
|
|
@click="compareDatabases"
|
|
>
|
|
<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="chart"
|
|
svg-class="-ml-1 mr-3 h-5 w-5"
|
|
/>
|
|
Compare Databases
|
|
</button>
|
|
|
|
<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"
|
|
@click="migrateContacts"
|
|
>
|
|
<IconRenderer icon-name="plus" svg-class="-ml-1 mr-3 h-5 w-5" />
|
|
Migrate Contacts
|
|
</button>
|
|
|
|
<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-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"
|
|
>
|
|
<IconRenderer icon-name="settings" svg-class="-ml-1 mr-3 h-5 w-5" />
|
|
Migrate Settings
|
|
</button>
|
|
|
|
<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-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"
|
|
>
|
|
<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-teal-600 hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="verifyMigration"
|
|
>
|
|
<IconRenderer icon-name="check" svg-class="-ml-1 mr-3 h-5 w-5" />
|
|
Verify Migration
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="text-center py-12">
|
|
<div
|
|
class="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md text-white bg-blue-500 hover:bg-blue-400 transition ease-in-out duration-150 cursor-not-allowed"
|
|
>
|
|
<IconRenderer
|
|
icon-name="spinner"
|
|
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
fill="currentColor"
|
|
/>
|
|
{{ loadingMessage }}
|
|
</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 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="chart"
|
|
svg-class="h-6 w-6 text-blue-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Dexie Contacts
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.dexieContacts.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-6 w-6 text-green-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
SQLite Contacts
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.sqliteContacts.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="settings"
|
|
svg-class="h-6 w-6 text-purple-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Dexie Settings
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.dexieSettings.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-6 w-6 text-indigo-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
SQLite Settings
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.sqliteSettings.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="lock"
|
|
svg-class="h-6 w-6 text-orange-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Dexie Accounts
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.dexieAccounts.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-6 w-6 text-teal-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
SQLite Accounts
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.sqliteAccounts.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Differences Section -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Contacts Differences -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
|
Contact Differences
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="plusCircle"
|
|
svg-class="h-5 w-5 text-blue-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-blue-900">Added</span>
|
|
</div>
|
|
<span class="text-sm font-bold text-blue-900">{{
|
|
comparison.differences.contacts.added.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="edit"
|
|
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-yellow-900"
|
|
>Modified</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-yellow-900">{{
|
|
comparison.differences.contacts.modified.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="trash"
|
|
svg-class="h-5 w-5 text-red-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-red-900"
|
|
>Missing</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-red-900">{{
|
|
comparison.differences.contacts.missing.length
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Details -->
|
|
<div
|
|
v-if="comparison.differences.contacts.added.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Added Contacts:
|
|
</h4>
|
|
<div class="max-h-32 overflow-y-auto space-y-1">
|
|
<div
|
|
v-for="contact in comparison.differences.contacts.added"
|
|
:key="contact.did"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
{{ contact.name || "Unnamed" }} ({{
|
|
contact.did.substring(0, 20)
|
|
}}...)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Differences -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
|
Settings Differences
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="plusCircle"
|
|
svg-class="h-5 w-5 text-blue-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-blue-900">Added</span>
|
|
</div>
|
|
<span class="text-sm font-bold text-blue-900">{{
|
|
comparison.differences.settings.added.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="edit"
|
|
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-yellow-900"
|
|
>Modified</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-yellow-900">{{
|
|
comparison.differences.settings.modified.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="trash"
|
|
svg-class="h-5 w-5 text-red-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-red-900"
|
|
>Missing</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-red-900">{{
|
|
comparison.differences.settings.missing.length
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Details -->
|
|
<div
|
|
v-if="comparison.differences.settings.added.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Added Settings:
|
|
</h4>
|
|
<div class="max-h-32 overflow-y-auto space-y-1">
|
|
<div
|
|
v-for="setting in comparison.differences.settings.added"
|
|
:key="setting.id"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
ID: {{ setting.id }} - {{ setting.firstName || "Unnamed" }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account Differences -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
|
Account Differences
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="plusCircle"
|
|
svg-class="h-5 w-5 text-blue-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-blue-900">Added</span>
|
|
</div>
|
|
<span class="text-sm font-bold text-blue-900">{{
|
|
comparison.differences.accounts.added.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="edit"
|
|
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-yellow-900"
|
|
>Modified</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-yellow-900">{{
|
|
comparison.differences.accounts.modified.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="trash"
|
|
svg-class="h-5 w-5 text-red-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-red-900"
|
|
>Missing</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-red-900">{{
|
|
comparison.differences.accounts.missing.length
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account Details -->
|
|
<div
|
|
v-if="comparison.differences.accounts.added.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Added Accounts:
|
|
</h4>
|
|
<div class="max-h-32 overflow-y-auto space-y-1">
|
|
<div
|
|
v-for="account in comparison.differences.accounts.added"
|
|
:key="account.id"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
ID: {{ account.id }} - {{ account.did.substring(0, 20) }}...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Migration Options -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
|
Migration Options
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div class="flex items-center">
|
|
<input
|
|
id="overwrite-existing"
|
|
v-model="overwriteExisting"
|
|
type="checkbox"
|
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
<label
|
|
for="overwrite-existing"
|
|
class="ml-2 block text-sm text-gray-900"
|
|
>
|
|
Overwrite existing records in SQLite
|
|
</label>
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-600">
|
|
When enabled, existing records in SQLite will be updated with
|
|
data from Dexie. When disabled, existing records will be skipped
|
|
during migration.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import IconRenderer from "../components/IconRenderer.vue";
|
|
import {
|
|
compareDatabases,
|
|
migrateContacts,
|
|
migrateSettings,
|
|
migrateAccounts,
|
|
generateComparisonYaml,
|
|
type DataComparison,
|
|
type MigrationResult,
|
|
} from "../services/migrationService";
|
|
import { USE_DEXIE_DB } from "../constants/app";
|
|
import { logger } from "../utils/logger";
|
|
|
|
/**
|
|
* Database Migration View Component
|
|
*
|
|
* This component provides a user interface for comparing and migrating data
|
|
* between Dexie (IndexedDB) and SQLite databases. It allows users to:
|
|
*
|
|
* 1. Compare data between the two databases
|
|
* 2. View differences in contacts and settings
|
|
* 3. Migrate contacts from Dexie to SQLite
|
|
* 4. Migrate settings from Dexie to SQLite
|
|
* 5. Export comparison results
|
|
*
|
|
* The component includes comprehensive error handling, loading states,
|
|
* and user-friendly feedback for all operations.
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
* @since 2024
|
|
*/
|
|
@Component({
|
|
name: "DatabaseMigration",
|
|
components: {
|
|
IconRenderer,
|
|
},
|
|
})
|
|
export default class DatabaseMigration extends Vue {
|
|
// Component state
|
|
private isLoading = false;
|
|
private loadingMessage = "";
|
|
private error = "";
|
|
private successMessage = "";
|
|
private comparison: DataComparison | null = null;
|
|
private overwriteExisting = false;
|
|
|
|
/**
|
|
* Computed property to check if Dexie database is enabled
|
|
*
|
|
* @returns {boolean} True if Dexie database is enabled, false otherwise
|
|
*/
|
|
get isDexieEnabled(): boolean {
|
|
return USE_DEXIE_DB;
|
|
}
|
|
|
|
/**
|
|
* Compares data between Dexie and SQLite databases
|
|
*
|
|
* This method retrieves data from both databases and identifies
|
|
* differences. It provides comprehensive feedback and error handling.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async compareDatabases(): Promise<void> {
|
|
this.setLoading("Comparing databases...");
|
|
this.clearMessages();
|
|
|
|
try {
|
|
this.comparison = await compareDatabases();
|
|
const totalItems =
|
|
this.comparison.differences.contacts.added.length +
|
|
this.comparison.differences.settings.added.length +
|
|
this.comparison.differences.accounts.added.length;
|
|
this.successMessage = `Comparison completed successfully. Found ${totalItems} items to migrate.`;
|
|
logger.info(
|
|
"[DatabaseMigration] Database comparison completed successfully",
|
|
);
|
|
} catch (error) {
|
|
this.error = `Failed to compare databases: ${error}`;
|
|
logger.error("[DatabaseMigration] Database comparison failed:", error);
|
|
} finally {
|
|
this.setLoading("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrates contacts from Dexie to SQLite database
|
|
*
|
|
* This method transfers contacts from the Dexie database to SQLite,
|
|
* with options to overwrite existing records.
|
|
*
|
|
* @async
|
|
* @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,
|
|
);
|
|
} 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("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrates settings from Dexie to SQLite database
|
|
*
|
|
* This method transfers settings from the Dexie database to SQLite,
|
|
* with options to overwrite existing records.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async migrateSettings(): Promise<void> {
|
|
this.setLoading("Migrating settings...");
|
|
this.clearMessages();
|
|
|
|
try {
|
|
const result: MigrationResult = await migrateSettings(
|
|
this.overwriteExisting,
|
|
);
|
|
|
|
if (result.success) {
|
|
this.successMessage = `Successfully migrated ${result.settingsMigrated} settings.`;
|
|
if (result.warnings.length > 0) {
|
|
this.successMessage += ` ${result.warnings.length} warnings.`;
|
|
}
|
|
logger.info(
|
|
"[DatabaseMigration] Settings migration completed successfully",
|
|
result,
|
|
);
|
|
} else {
|
|
this.error = `Migration failed: ${result.errors.join(", ")}`;
|
|
logger.error(
|
|
"[DatabaseMigration] Settings migration failed:",
|
|
result.errors,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.error = `Failed to migrate settings: ${error}`;
|
|
logger.error("[DatabaseMigration] Settings migration failed:", error);
|
|
} finally {
|
|
this.setLoading("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrates accounts from Dexie to SQLite database
|
|
*
|
|
* This method transfers accounts from the Dexie database to SQLite,
|
|
* with options to overwrite existing records. For accounts with mnemonic
|
|
* data, it uses the importFromMnemonic utility for proper key derivation.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async migrateAccounts(): Promise<void> {
|
|
this.setLoading("Migrating accounts...");
|
|
this.clearMessages();
|
|
|
|
try {
|
|
const result: MigrationResult = await migrateAccounts(
|
|
this.overwriteExisting,
|
|
);
|
|
|
|
if (result.success) {
|
|
this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`;
|
|
if (result.warnings.length > 0) {
|
|
this.successMessage += ` ${result.warnings.length} warnings.`;
|
|
}
|
|
logger.info(
|
|
"[DatabaseMigration] Account migration completed successfully",
|
|
result,
|
|
);
|
|
} else {
|
|
this.error = `Migration failed: ${result.errors.join(", ")}`;
|
|
logger.error(
|
|
"[DatabaseMigration] Account migration failed:",
|
|
result.errors,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.error = `Failed to migrate accounts: ${error}`;
|
|
logger.error("[DatabaseMigration] Account migration failed:", error);
|
|
} finally {
|
|
this.setLoading("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifies the migration by running another comparison
|
|
*
|
|
* This method runs a fresh comparison between Dexie and SQLite databases
|
|
* to verify that the migration was successful. It's useful to run this
|
|
* after completing migrations to ensure data integrity and relationship
|
|
* preservation.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async verifyMigration(): Promise<void> {
|
|
this.setLoading("Verifying migration...");
|
|
this.clearMessages();
|
|
|
|
try {
|
|
const newComparison = await compareDatabases();
|
|
|
|
// Check if there are any remaining differences
|
|
const totalRemaining =
|
|
newComparison.differences.contacts.added.length +
|
|
newComparison.differences.contacts.modified.length +
|
|
newComparison.differences.contacts.missing.length +
|
|
newComparison.differences.settings.added.length +
|
|
newComparison.differences.settings.modified.length +
|
|
newComparison.differences.settings.missing.length +
|
|
newComparison.differences.accounts.added.length +
|
|
newComparison.differences.accounts.modified.length +
|
|
newComparison.differences.accounts.missing.length;
|
|
|
|
if (totalRemaining === 0) {
|
|
this.successMessage =
|
|
"✅ Migration verification successful! All data has been migrated correctly.";
|
|
logger.info(
|
|
"[DatabaseMigration] Migration verification successful - no differences found",
|
|
);
|
|
} else {
|
|
this.successMessage = `⚠️ Migration verification completed. Found ${totalRemaining} remaining differences. Consider running additional migrations if needed.`;
|
|
logger.warn(
|
|
"[DatabaseMigration] Migration verification found remaining differences",
|
|
{
|
|
remaining: totalRemaining,
|
|
differences: newComparison.differences,
|
|
},
|
|
);
|
|
}
|
|
|
|
// Update the comparison to show the current state
|
|
this.comparison = newComparison;
|
|
} catch (error) {
|
|
this.error = `Failed to verify migration: ${error}`;
|
|
logger.error("[DatabaseMigration] Migration verification failed:", error);
|
|
} finally {
|
|
this.setLoading("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exports comparison results to a file
|
|
*
|
|
* This method generates a YAML-formatted comparison and triggers
|
|
* a file download for the user.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async exportComparison(): Promise<void> {
|
|
if (!this.comparison) {
|
|
this.error = "No comparison data available to export";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const yamlData = generateComparisonYaml(this.comparison);
|
|
const blob = new Blob([yamlData], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = `database-comparison-${new Date().toISOString().split("T")[0]}.json`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
|
|
this.successMessage = "Comparison exported successfully";
|
|
logger.info("[DatabaseMigration] Comparison exported successfully");
|
|
} catch (error) {
|
|
this.error = `Failed to export comparison: ${error}`;
|
|
logger.error("[DatabaseMigration] Export failed:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the loading state and message
|
|
*
|
|
* @param {string} message - The loading message to display
|
|
*/
|
|
private setLoading(message: string): void {
|
|
this.isLoading = message !== "";
|
|
this.loadingMessage = message;
|
|
}
|
|
|
|
/**
|
|
* Clears all error and success messages
|
|
*/
|
|
private clearMessages(): void {
|
|
this.error = "";
|
|
this.successMessage = "";
|
|
}
|
|
}
|
|
</script>
|
|
|