@ -22,12 +22,14 @@
* /
* /
import { PlatformServiceFactory } from "./PlatformServiceFactory" ;
import { PlatformServiceFactory } from "./PlatformServiceFactory" ;
import { db } from "../db/index" ;
import { db , accountsDBPromise } from "../db/index" ;
import { Contact , ContactMethod } from "../db/tables/contacts" ;
import { Contact , ContactMethod } from "../db/tables/contacts" ;
import { Settings } from "../db/tables/settings" ;
import { Settings } from "../db/tables/settings" ;
import { Account } from "../db/tables/accounts" ;
import { logger } from "../utils/logger" ;
import { logger } from "../utils/logger" ;
import { parseJsonField } from "../db/databaseUtil" ;
import { parseJsonField } from "../db/databaseUtil" ;
import { USE_DEXIE_DB } from "../constants/app" ;
import { USE_DEXIE_DB } from "../constants/app" ;
import { importFromMnemonic } from "../libs/util" ;
/ * *
/ * *
* Interface for data comparison results between Dexie and SQLite databases
* Interface for data comparison results between Dexie and SQLite databases
@ -41,6 +43,8 @@ import { USE_DEXIE_DB } from "../constants/app";
* @property { Contact [ ] } sqliteContacts - All contacts from SQLite database
* @property { Contact [ ] } sqliteContacts - All contacts from SQLite database
* @property { Settings [ ] } dexieSettings - All settings from Dexie database
* @property { Settings [ ] } dexieSettings - All settings from Dexie database
* @property { Settings [ ] } sqliteSettings - All settings from SQLite database
* @property { Settings [ ] } sqliteSettings - All settings from SQLite database
* @property { Account [ ] } dexieAccounts - All accounts from Dexie database
* @property { Account [ ] } sqliteAccounts - All accounts from SQLite database
* @property { Object } differences - Detailed differences between databases
* @property { Object } differences - Detailed differences between databases
* @property { Object } differences . contacts - Contact - specific differences
* @property { Object } differences . contacts - Contact - specific differences
* @property { Contact [ ] } differences . contacts . added - Contacts in Dexie but not SQLite
* @property { Contact [ ] } differences . contacts . added - Contacts in Dexie but not SQLite
@ -50,12 +54,18 @@ import { USE_DEXIE_DB } from "../constants/app";
* @property { Settings [ ] } differences . settings . added - Settings in Dexie but not SQLite
* @property { Settings [ ] } differences . settings . added - Settings in Dexie but not SQLite
* @property { Settings [ ] } differences . settings . modified - Settings that differ between databases
* @property { Settings [ ] } differences . settings . modified - Settings that differ between databases
* @property { Settings [ ] } differences . settings . missing - Settings in SQLite but not Dexie
* @property { Settings [ ] } differences . settings . missing - Settings in SQLite but not Dexie
* @property { Object } differences . accounts - Account - specific differences
* @property { Account [ ] } differences . accounts . added - Accounts in Dexie but not SQLite
* @property { Account [ ] } differences . accounts . modified - Accounts that differ between databases
* @property { Account [ ] } differences . accounts . missing - Accounts in SQLite but not Dexie
* /
* /
export interface DataComparison {
export interface DataComparison {
dexieContacts : Contact [ ] ;
dexieContacts : Contact [ ] ;
sqliteContacts : Contact [ ] ;
sqliteContacts : Contact [ ] ;
dexieSettings : Settings [ ] ;
dexieSettings : Settings [ ] ;
sqliteSettings : Settings [ ] ;
sqliteSettings : Settings [ ] ;
dexieAccounts : Account [ ] ;
sqliteAccounts : Account [ ] ;
differences : {
differences : {
contacts : {
contacts : {
added : Contact [ ] ;
added : Contact [ ] ;
@ -67,6 +77,11 @@ export interface DataComparison {
modified : Settings [ ] ;
modified : Settings [ ] ;
missing : Settings [ ] ;
missing : Settings [ ] ;
} ;
} ;
accounts : {
added : Account [ ] ;
modified : Account [ ] ;
missing : Account [ ] ;
} ;
} ;
} ;
}
}
@ -81,6 +96,7 @@ export interface DataComparison {
* @property { boolean } success - Whether the migration operation completed successfully
* @property { boolean } success - Whether the migration operation completed successfully
* @property { number } contactsMigrated - Number of contacts successfully migrated
* @property { number } contactsMigrated - Number of contacts successfully migrated
* @property { number } settingsMigrated - Number of settings successfully migrated
* @property { number } settingsMigrated - Number of settings successfully migrated
* @property { number } accountsMigrated - Number of accounts successfully migrated
* @property { string [ ] } errors - Array of error messages encountered during migration
* @property { string [ ] } errors - Array of error messages encountered during migration
* @property { string [ ] } warnings - Array of warning messages ( non - fatal issues )
* @property { string [ ] } warnings - Array of warning messages ( non - fatal issues )
* /
* /
@ -88,6 +104,7 @@ export interface MigrationResult {
success : boolean ;
success : boolean ;
contactsMigrated : number ;
contactsMigrated : number ;
settingsMigrated : number ;
settingsMigrated : number ;
accountsMigrated : number ;
errors : string [ ] ;
errors : string [ ] ;
warnings : string [ ] ;
warnings : string [ ] ;
}
}
@ -315,6 +332,105 @@ export async function getSqliteSettings(): Promise<Settings[]> {
}
}
}
}
/ * *
* Retrieves all accounts from the SQLite database
*
* This function uses the platform service to query the SQLite database
* and retrieve all account records . It handles the conversion of raw
* database results into properly typed Account objects .
*
* The function also handles JSON parsing for complex fields like
* identity , ensuring proper type conversion .
*
* @async
* @function getSqliteAccounts
* @returns { Promise < Account [ ] > } Array of all accounts from SQLite database
* @throws { Error } If database query fails or data conversion fails
* @example
* ` ` ` typescript
* try {
* const accounts = await getSqliteAccounts ( ) ;
* console . log ( ` Retrieved ${ accounts . length } accounts from SQLite ` ) ;
* } catch ( error ) {
* console . error ( 'Failed to retrieve SQLite accounts:' , error ) ;
* }
* ` ` `
* /
export async function getSqliteAccounts ( ) : Promise < Account [ ] > {
try {
const platformService = PlatformServiceFactory . getInstance ( ) ;
const result = await platformService . dbQuery ( "SELECT * FROM accounts" ) ;
if ( ! result ? . values ? . length ) {
return [ ] ;
}
const accounts = result . values . map ( ( row ) = > {
const account = parseJsonField ( row , { } ) as any ;
return {
id : account.id ,
dateCreated : account.dateCreated || "" ,
derivationPath : account.derivationPath || "" ,
did : account.did || "" ,
identity : account.identity || "" ,
mnemonic : account.mnemonic || "" ,
passkeyCredIdHex : account.passkeyCredIdHex || "" ,
publicKeyHex : account.publicKeyHex || "" ,
} as Account ;
} ) ;
logger . info (
` [MigrationService] Retrieved ${ accounts . length } accounts from SQLite ` ,
) ;
return accounts ;
} catch ( error ) {
logger . error ( "[MigrationService] Error retrieving SQLite accounts:" , error ) ;
throw new Error ( ` Failed to retrieve SQLite accounts: ${ error } ` ) ;
}
}
/ * *
* Retrieves all accounts from the Dexie ( IndexedDB ) database
*
* This function connects to the Dexie database and retrieves all account
* records . It requires that USE_DEXIE_DB is enabled in the app constants .
*
* The function handles database opening and error conditions , providing
* detailed logging for debugging purposes .
*
* @async
* @function getDexieAccounts
* @returns { Promise < Account [ ] > } Array of all accounts from Dexie database
* @throws { Error } If Dexie database is not enabled or if database access fails
* @example
* ` ` ` typescript
* try {
* const accounts = await getDexieAccounts ( ) ;
* console . log ( ` Retrieved ${ accounts . length } accounts from Dexie ` ) ;
* } catch ( error ) {
* console . error ( 'Failed to retrieve Dexie accounts:' , error ) ;
* }
* ` ` `
* /
export async function getDexieAccounts ( ) : Promise < Account [ ] > {
if ( ! USE_DEXIE_DB ) {
throw new Error ( "Dexie database is not enabled" ) ;
}
try {
const accountsDB = await accountsDBPromise ;
await accountsDB . open ( ) ;
const accounts = await accountsDB . accounts . toArray ( ) ;
logger . info (
` [MigrationService] Retrieved ${ accounts . length } accounts from Dexie ` ,
) ;
return accounts ;
} catch ( error ) {
logger . error ( "[MigrationService] Error retrieving Dexie accounts:" , error ) ;
throw new Error ( ` Failed to retrieve Dexie accounts: ${ error } ` ) ;
}
}
/ * *
/ * *
* Compares data between Dexie and SQLite databases
* Compares data between Dexie and SQLite databases
*
*
@ -346,13 +462,21 @@ export async function getSqliteSettings(): Promise<Settings[]> {
export async function compareDatabases ( ) : Promise < DataComparison > {
export async function compareDatabases ( ) : Promise < DataComparison > {
logger . info ( "[MigrationService] Starting database comparison" ) ;
logger . info ( "[MigrationService] Starting database comparison" ) ;
const [ dexieContacts , sqliteContacts , dexieSettings , sqliteSettings ] =
const [
await Promise . all ( [
dexieContacts ,
getDexieContacts ( ) ,
sqliteContacts ,
getSqliteContacts ( ) ,
dexieSettings ,
getDexieSettings ( ) ,
sqliteSettings ,
getSqliteSettings ( ) ,
dexieAccounts ,
] ) ;
sqliteAccounts ,
] = await Promise . all ( [
getDexieContacts ( ) ,
getSqliteContacts ( ) ,
getDexieSettings ( ) ,
getSqliteSettings ( ) ,
getDexieAccounts ( ) ,
getSqliteAccounts ( ) ,
] ) ;
// Compare contacts
// Compare contacts
const contactDifferences = compareContacts ( dexieContacts , sqliteContacts ) ;
const contactDifferences = compareContacts ( dexieContacts , sqliteContacts ) ;
@ -360,14 +484,20 @@ export async function compareDatabases(): Promise<DataComparison> {
// Compare settings
// Compare settings
const settingsDifferences = compareSettings ( dexieSettings , sqliteSettings ) ;
const settingsDifferences = compareSettings ( dexieSettings , sqliteSettings ) ;
// Compare accounts
const accountDifferences = compareAccounts ( dexieAccounts , sqliteAccounts ) ;
const comparison : DataComparison = {
const comparison : DataComparison = {
dexieContacts ,
dexieContacts ,
sqliteContacts ,
sqliteContacts ,
dexieSettings ,
dexieSettings ,
sqliteSettings ,
sqliteSettings ,
dexieAccounts ,
sqliteAccounts ,
differences : {
differences : {
contacts : contactDifferences ,
contacts : contactDifferences ,
settings : settingsDifferences ,
settings : settingsDifferences ,
accounts : accountDifferences ,
} ,
} ,
} ;
} ;
@ -376,8 +506,11 @@ export async function compareDatabases(): Promise<DataComparison> {
sqliteContacts : sqliteContacts.length ,
sqliteContacts : sqliteContacts.length ,
dexieSettings : dexieSettings.length ,
dexieSettings : dexieSettings.length ,
sqliteSettings : sqliteSettings.length ,
sqliteSettings : sqliteSettings.length ,
dexieAccounts : dexieAccounts.length ,
sqliteAccounts : sqliteAccounts.length ,
contactDifferences : contactDifferences ,
contactDifferences : contactDifferences ,
settingsDifferences : settingsDifferences ,
settingsDifferences : settingsDifferences ,
accountDifferences : accountDifferences ,
} ) ;
} ) ;
return comparison ;
return comparison ;
@ -491,6 +624,57 @@ function compareSettings(
return { added , modified , missing } ;
return { added , modified , missing } ;
}
}
/ * *
* Compares accounts between Dexie and SQLite databases
*
* This helper function analyzes two arrays of accounts and identifies
* which accounts are added ( in Dexie but not SQLite ) , modified
* ( different between databases ) , or missing ( in SQLite but not Dexie ) .
*
* The comparison is based on the account ' s ID as the primary key ,
* with detailed field - by - field comparison for modified accounts .
*
* @function compareAccounts
* @param { Account [ ] } dexieAccounts - Accounts from Dexie database
* @param { Account [ ] } sqliteAccounts - Accounts from SQLite database
* @returns { Object } Object containing added , modified , and missing accounts
* @returns { Account [ ] } returns . added - Accounts in Dexie but not SQLite
* @returns { Account [ ] } returns . modified - Accounts that differ between databases
* @returns { Account [ ] } returns . missing - Accounts in SQLite but not Dexie
* @example
* ` ` ` typescript
* const differences = compareAccounts ( dexieAccounts , sqliteAccounts ) ;
* console . log ( ` Added: ${ differences . added . length } ` ) ;
* console . log ( ` Modified: ${ differences . modified . length } ` ) ;
* console . log ( ` Missing: ${ differences . missing . length } ` ) ;
* ` ` `
* /
function compareAccounts ( dexieAccounts : Account [ ] , sqliteAccounts : Account [ ] ) {
const added : Account [ ] = [ ] ;
const modified : Account [ ] = [ ] ;
const missing : Account [ ] = [ ] ;
// Find accounts that exist in Dexie but not in SQLite
for ( const dexieAccount of dexieAccounts ) {
const sqliteAccount = sqliteAccounts . find ( ( a ) = > a . id === dexieAccount . id ) ;
if ( ! sqliteAccount ) {
added . push ( dexieAccount ) ;
} else if ( ! accountsEqual ( dexieAccount , sqliteAccount ) ) {
modified . push ( dexieAccount ) ;
}
}
// Find accounts that exist in SQLite but not in Dexie
for ( const sqliteAccount of sqliteAccounts ) {
const dexieAccount = dexieAccounts . find ( ( a ) = > a . id === sqliteAccount . id ) ;
if ( ! dexieAccount ) {
missing . push ( sqliteAccount ) ;
}
}
return { added , modified , missing } ;
}
/ * *
/ * *
* Compares two contacts for equality
* Compares two contacts for equality
*
*
@ -592,6 +776,43 @@ function settingsEqual(settings1: Settings, settings2: Settings): boolean {
) ;
) ;
}
}
/ * *
* Compares two accounts for equality
*
* This helper function performs a deep comparison of two Account objects
* to determine if they are identical . The comparison includes all
* relevant fields including complex objects like identity .
*
* For identity , the function uses JSON . stringify to compare
* the objects , ensuring that both structure and content are identical .
*
* @function accountsEqual
* @param { Account } account1 - First account to compare
* @param { Account } account2 - Second account to compare
* @returns { boolean } True if accounts are identical , false otherwise
* @example
* ` ` ` typescript
* const areEqual = accountsEqual ( account1 , account2 ) ;
* if ( areEqual ) {
* console . log ( 'Accounts are identical' ) ;
* } else {
* console . log ( 'Accounts differ' ) ;
* }
* ` ` `
* /
function accountsEqual ( account1 : Account , account2 : Account ) : boolean {
return (
account1 . id === account2 . id &&
account1 . dateCreated === account2 . dateCreated &&
account1 . derivationPath === account2 . derivationPath &&
account1 . did === account2 . did &&
account1 . identity === account2 . identity &&
account1 . mnemonic === account2 . mnemonic &&
account1 . passkeyCredIdHex === account2 . passkeyCredIdHex &&
account1 . publicKeyHex === account2 . publicKeyHex
) ;
}
/ * *
/ * *
* Generates YAML - formatted comparison data
* Generates YAML - formatted comparison data
*
*
@ -622,6 +843,8 @@ export function generateComparisonYaml(comparison: DataComparison): string {
sqliteContacts : comparison.sqliteContacts.length ,
sqliteContacts : comparison.sqliteContacts.length ,
dexieSettings : comparison.dexieSettings.length ,
dexieSettings : comparison.dexieSettings.length ,
sqliteSettings : comparison.sqliteSettings.length ,
sqliteSettings : comparison.sqliteSettings.length ,
dexieAccounts : comparison.dexieAccounts.length ,
sqliteAccounts : comparison.sqliteAccounts.length ,
} ,
} ,
differences : {
differences : {
contacts : {
contacts : {
@ -634,6 +857,11 @@ export function generateComparisonYaml(comparison: DataComparison): string {
modified : comparison.differences.settings.modified.length ,
modified : comparison.differences.settings.modified.length ,
missing : comparison.differences.settings.missing.length ,
missing : comparison.differences.settings.missing.length ,
} ,
} ,
accounts : {
added : comparison.differences.accounts.added.length ,
modified : comparison.differences.accounts.modified.length ,
missing : comparison.differences.accounts.missing.length ,
} ,
} ,
} ,
contacts : {
contacts : {
dexie : comparison.dexieContacts.map ( ( c ) = > ( {
dexie : comparison.dexieContacts.map ( ( c ) = > ( {
@ -677,6 +905,28 @@ export function generateComparisonYaml(comparison: DataComparison): string {
searchBoxes : s.searchBoxes ,
searchBoxes : s.searchBoxes ,
} ) ) ,
} ) ) ,
} ,
} ,
accounts : {
dexie : comparison.dexieAccounts.map ( ( a ) = > ( {
id : a.id ,
dateCreated : a.dateCreated ,
derivationPath : a.derivationPath ,
did : a.did ,
identity : a.identity ,
mnemonic : a.mnemonic ,
passkeyCredIdHex : a.passkeyCredIdHex ,
publicKeyHex : a.publicKeyHex ,
} ) ) ,
sqlite : comparison.sqliteAccounts.map ( ( a ) = > ( {
id : a.id ,
dateCreated : a.dateCreated ,
derivationPath : a.derivationPath ,
did : a.did ,
identity : a.identity ,
mnemonic : a.mnemonic ,
passkeyCredIdHex : a.passkeyCredIdHex ,
publicKeyHex : a.publicKeyHex ,
} ) ) ,
} ,
} ,
} ,
} ;
} ;
@ -725,6 +975,7 @@ export async function migrateContacts(
success : true ,
success : true ,
contactsMigrated : 0 ,
contactsMigrated : 0 ,
settingsMigrated : 0 ,
settingsMigrated : 0 ,
accountsMigrated : 0 ,
errors : [ ] ,
errors : [ ] ,
warnings : [ ] ,
warnings : [ ] ,
} ;
} ;
@ -834,6 +1085,7 @@ export async function migrateSettings(
success : true ,
success : true ,
contactsMigrated : 0 ,
contactsMigrated : 0 ,
settingsMigrated : 0 ,
settingsMigrated : 0 ,
accountsMigrated : 0 ,
errors : [ ] ,
errors : [ ] ,
warnings : [ ] ,
warnings : [ ] ,
} ;
} ;
@ -921,6 +1173,144 @@ export async function migrateSettings(
}
}
}
}
/ * *
* Migrates accounts from Dexie to SQLite database
*
* This function transfers all accounts from the Dexie database to the
* SQLite database . It handles both new accounts ( INSERT ) and existing
* accounts ( UPDATE ) based on the overwriteExisting parameter .
*
* For accounts with mnemonic data , the function uses importFromMnemonic
* to ensure proper key derivation and identity creation during migration .
*
* The function processes accounts one by one to ensure data integrity
* and provides detailed logging of the migration process . It returns
* comprehensive results including success status , counts , and any
* errors or warnings encountered .
*
* @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
* ` ` ` typescript
* try {
* const result = await migrateAccounts ( true ) ; // Overwrite existing
* if ( result . success ) {
* console . log ( ` Successfully migrated ${ result . accountsMigrated } accounts ` ) ;
* } else {
* console . error ( 'Migration failed:' , result . errors ) ;
* }
* } catch ( error ) {
* console . error ( 'Migration process failed:' , error ) ;
* }
* ` ` `
* /
export async function migrateAccounts (
overwriteExisting : boolean = false ,
) : Promise < MigrationResult > {
logger . info ( "[MigrationService] Starting account migration" , {
overwriteExisting ,
} ) ;
const result : MigrationResult = {
success : true ,
contactsMigrated : 0 ,
settingsMigrated : 0 ,
accountsMigrated : 0 ,
errors : [ ] ,
warnings : [ ] ,
} ;
try {
const dexieAccounts = await getDexieAccounts ( ) ;
const platformService = PlatformServiceFactory . getInstance ( ) ;
for ( const account of dexieAccounts ) {
try {
// Check if account already exists
const existingResult = await platformService . dbQuery (
"SELECT id FROM accounts WHERE id = ?" ,
[ account . id ] ,
) ;
if ( existingResult ? . values ? . length ) {
if ( overwriteExisting ) {
// Update existing account
const { sql , params } = generateUpdateStatement (
account as unknown as Record < string , unknown > ,
"accounts" ,
"id = ?" ,
[ account . id ] ,
) ;
await platformService . dbExec ( sql , params ) ;
result . accountsMigrated ++ ;
logger . info ( ` [MigrationService] Updated account: ${ account . id } ` ) ;
} else {
result . warnings . push (
` Account ${ account . id } already exists, skipping ` ,
) ;
}
} else {
// For new accounts with mnemonic, use importFromMnemonic for proper key derivation
if ( account . mnemonic && account . derivationPath ) {
try {
// Use importFromMnemonic to ensure proper key derivation and identity creation
await importFromMnemonic (
account . mnemonic ,
account . derivationPath ,
false , // Don't erase existing accounts during migration
) ;
logger . info (
` [MigrationService] Imported account with mnemonic: ${ account . id } ` ,
) ;
} catch ( importError ) {
// Fall back to direct insertion if importFromMnemonic fails
logger . warn (
` [MigrationService] importFromMnemonic failed for account ${ account . id } , falling back to direct insertion: ${ importError } ` ,
) ;
const { sql , params } = generateInsertStatement (
account as unknown as Record < string , unknown > ,
"accounts" ,
) ;
await platformService . dbExec ( sql , params ) ;
}
} else {
// Insert new account without mnemonic
const { sql , params } = generateInsertStatement (
account as unknown as Record < string , unknown > ,
"accounts" ,
) ;
await platformService . dbExec ( sql , params ) ;
}
result . accountsMigrated ++ ;
logger . info ( ` [MigrationService] Added account: ${ account . id } ` ) ;
}
} catch ( error ) {
const errorMsg = ` Failed to migrate account ${ account . id } : ${ error } ` ;
logger . error ( "[MigrationService]" , errorMsg ) ;
result . errors . push ( errorMsg ) ;
result . success = false ;
}
}
logger . info ( "[MigrationService] Account migration completed" , {
accountsMigrated : result.accountsMigrated ,
errors : result.errors.length ,
warnings : result.warnings.length ,
} ) ;
return result ;
} catch ( error ) {
const errorMsg = ` Account migration failed: ${ error } ` ;
logger . error ( "[MigrationService]" , errorMsg ) ;
result . errors . push ( errorMsg ) ;
result . success = false ;
return result ;
}
}
/ * *
/ * *
* Generates SQL INSERT statement and parameters from a model object
* Generates SQL INSERT statement and parameters from a model object
*
*