diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts index 1b496b21..b4db6c3b 100644 --- a/src/db/tables/contacts.ts +++ b/src/db/tables/contacts.ts @@ -19,6 +19,10 @@ export interface Contact { registered?: boolean; // cached value of the server setting } +export type ContactWithJsonStrings = Contact & { + contactMethods?: string; +} + export const ContactSchema = { contacts: "&did, name", // no need to key by other things }; diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index 726a41ee..bcf33982 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -64,6 +64,11 @@ export type Settings = { webPushServer?: string; // Web Push server URL }; +// type of settings where the searchBoxes are JSON strings instead of objects +export type SettingsWithJsonStrings = Settings & { + searchBoxes: string; +}; + export function checkIsAnyFeedFilterOn(settings: Settings): boolean { return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible); } diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 4145a1cf..ca1dcb35 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -26,11 +26,12 @@ import "dexie-export-import"; import { PlatformServiceFactory } from "./PlatformServiceFactory"; import { db, accountsDBPromise } from "../db/index"; import { Contact, ContactMethod } from "../db/tables/contacts"; -import { Settings, MASTER_SETTINGS_KEY } from "../db/tables/settings"; -import { Account } from "../db/tables/accounts"; +import { Settings, MASTER_SETTINGS_KEY, SettingsWithJsonStrings, BoundingBox } from "../db/tables/settings"; +import { Account, AccountEncrypted } from "../db/tables/accounts"; import { logger } from "../utils/logger"; -import { parseJsonField } from "../db/databaseUtil"; +import { mapColumnsToValues, parseJsonField } from "../db/databaseUtil"; import { importFromMnemonic } from "../libs/util"; +import { IIdentifier } from "@veramo/core"; /** * Interface for data comparison results between Dexie and SQLite databases @@ -66,22 +67,24 @@ export interface DataComparison { dexieSettings: Settings[]; sqliteSettings: Settings[]; dexieAccounts: Account[]; - sqliteAccounts: Account[]; + sqliteAccounts: string[]; differences: { contacts: { added: Contact[]; modified: Contact[]; + unmodified: Contact[]; missing: Contact[]; }; settings: { added: Settings[]; modified: Settings[]; + unmodified: Settings[]; missing: Settings[]; }; accounts: { added: Account[]; - modified: Account[]; - missing: Account[]; + unmodified: Account[]; + missing: string[]; }; }; } @@ -184,22 +187,14 @@ export async function getSqliteContacts(): Promise { let contacts: Contact[] = []; if (result?.values?.length) { - contacts = result.values.map((row) => { - const contact = parseJsonField(row, {}) as Contact; - return { - did: contact.did || "", - name: contact.name || "", - contactMethods: parseJsonField( - contact.contactMethods, - [], - ) as ContactMethod[], - nextPubKeyHashB64: contact.nextPubKeyHashB64 || "", - notes: contact.notes || "", - profileImageUrl: contact.profileImageUrl || "", - publicKeyBase64: contact.publicKeyBase64 || "", - seesMe: contact.seesMe || false, - registered: contact.registered || false, - } as Contact; + const preContacts = mapColumnsToValues(result.columns, result.values) as unknown as Contact[]; + // This is redundant since absurd-sql auto-parses JSON strings to objects. + // But we started it, and it should be known everywhere, so we're keeping it. + contacts = preContacts.map((contact) => { + if (contact.contactMethods) { + contact.contactMethods = parseJsonField(contact.contactMethods, []) as ContactMethod[]; + } + return contact; }); } @@ -281,37 +276,16 @@ export async function getSqliteSettings(): Promise { let settings: Settings[] = []; if (result?.values?.length) { - settings = result.values.map((row) => { - const setting = parseJsonField(row, {}) as Settings; - return { - id: setting.id, - accountDid: setting.accountDid || "", - activeDid: setting.activeDid || "", - apiServer: setting.apiServer || "", - filterFeedByNearby: setting.filterFeedByNearby || false, - filterFeedByVisible: setting.filterFeedByVisible || false, - finishedOnboarding: setting.finishedOnboarding || false, - firstName: setting.firstName || "", - hideRegisterPromptOnNewContact: setting.hideRegisterPromptOnNewContact || false, - isRegistered: setting.isRegistered || false, - lastName: setting.lastName || "", - lastAckedOfferToUserJwtId: setting.lastAckedOfferToUserJwtId || "", - lastAckedOfferToUserProjectsJwtId: setting.lastAckedOfferToUserProjectsJwtId || "", - lastNotifiedClaimId: setting.lastNotifiedClaimId || "", - lastViewedClaimId: setting.lastViewedClaimId || "", - notifyingNewActivityTime: setting.notifyingNewActivityTime || "", - notifyingReminderMessage: setting.notifyingReminderMessage || "", - notifyingReminderTime: setting.notifyingReminderTime || "", - partnerApiServer: setting.partnerApiServer || "", - passkeyExpirationMinutes: setting.passkeyExpirationMinutes, - profileImageUrl: setting.profileImageUrl || "", - searchBoxes: parseJsonField(setting.searchBoxes, []), - showContactGivesInline: setting.showContactGivesInline || false, - showGeneralAdvanced: setting.showGeneralAdvanced || false, - showShortcutBvc: setting.showShortcutBvc || false, - vapid: setting.vapid || "", - } as Settings; - }); + const presettings = + mapColumnsToValues(result.columns, result.values) as Settings[]; + // This is redundant since absurd-sql auto-parses JSON strings to objects. + // But we started it, and it should be known everywhere, so we're keeping it. + settings = presettings.map((setting) => { + if (setting.searchBoxes) { + setting.searchBoxes = parseJsonField(setting.searchBoxes, []) as Array<{ name: string, bbox: BoundingBox }>; + } + return setting; + }) } logger.info( @@ -336,7 +310,7 @@ export async function getSqliteSettings(): Promise { * * @async * @function getSqliteAccounts - * @returns {Promise} Array of all accounts from SQLite database + * @returns {Promise} Array of all accounts from SQLite database * @throws {Error} If database query fails or data conversion fails * @example * ```typescript @@ -348,32 +322,20 @@ export async function getSqliteSettings(): Promise { * } * ``` */ -export async function getSqliteAccounts(): Promise { +export async function getSqliteAccounts(): Promise { try { const platformService = PlatformServiceFactory.getInstance(); - const result = await platformService.dbQuery("SELECT * FROM accounts"); + const result = await platformService.dbQuery("SELECT did FROM accounts"); - let accounts: Account[] = []; + let dids: string[] = []; if (result?.values?.length) { - accounts = result.values.map((row) => { - const account = parseJsonField(row, {}) as Account; - 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; - }); + dids = result.values.map((row) => row[0] as string); } logger.info( - `[MigrationService] Retrieved ${accounts.length} accounts from SQLite`, + `[MigrationService] Retrieved ${dids.length} accounts from SQLite`, ); - return accounts; + return dids; } catch (error) { logger.error("[MigrationService] Error retrieving SQLite accounts:", error); throw new Error(`Failed to retrieve SQLite accounts: ${error}`); @@ -532,6 +494,7 @@ export async function compareDatabases(): Promise { function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) { const added: Contact[] = []; const modified: Contact[] = []; + const unmodified: Contact[] = []; const missing: Contact[] = []; // Find contacts that exist in Dexie but not in SQLite @@ -543,6 +506,8 @@ function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) { added.push(dexieContact); } else if (!contactsEqual(dexieContact, sqliteContact)) { modified.push(dexieContact); + } else { + unmodified.push(dexieContact); } } @@ -554,7 +519,7 @@ function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) { } } - return { added, modified, missing }; + return { added, modified, unmodified, missing }; } /** @@ -588,27 +553,30 @@ function compareSettings( ) { const added: Settings[] = []; const modified: Settings[] = []; + const unmodified: Settings[] = []; const missing: Settings[] = []; // Find settings that exist in Dexie but not in SQLite for (const dexieSetting of dexieSettings) { - const sqliteSetting = sqliteSettings.find((s) => s.id === dexieSetting.id); + const sqliteSetting = sqliteSettings.find((s) => s.accountDid === dexieSetting.accountDid); if (!sqliteSetting) { added.push(dexieSetting); } else if (!settingsEqual(dexieSetting, sqliteSetting)) { modified.push(dexieSetting); + } else { + unmodified.push(dexieSetting); } } // Find settings that exist in SQLite but not in Dexie for (const sqliteSetting of sqliteSettings) { - const dexieSetting = dexieSettings.find((s) => s.id === sqliteSetting.id); + const dexieSetting = dexieSettings.find((s) => s.accountDid === sqliteSetting.accountDid); if (!dexieSetting) { missing.push(sqliteSetting); } } - return { added, modified, missing }; + return { added, modified, unmodified, missing }; } /** @@ -623,11 +591,11 @@ function compareSettings( * * @function compareAccounts * @param {Account[]} dexieAccounts - Accounts from Dexie database - * @param {Account[]} sqliteAccounts - Accounts from SQLite database + * @param {Account[]} sqliteDids - 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 + * @returns {Account[]} returns.modified - always 0 because we don't check + * @returns {string[]} returns.missing - Accounts in SQLite but not Dexie * @example * ```typescript * const differences = compareAccounts(dexieAccounts, sqliteAccounts); @@ -636,30 +604,30 @@ function compareSettings( * console.log(`Missing: ${differences.missing.length}`); * ``` */ -function compareAccounts(dexieAccounts: Account[], sqliteAccounts: Account[]) { +function compareAccounts(dexieAccounts: Account[], sqliteDids: string[]) { const added: Account[] = []; - const modified: Account[] = []; - const missing: Account[] = []; + const unmodified: Account[] = []; + const missing: string[] = []; // 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) { + const sqliteDid = sqliteDids.find((a) => a === dexieAccount.did); + if (!sqliteDid) { added.push(dexieAccount); - } else if (!accountsEqual(dexieAccount, sqliteAccount)) { - modified.push(dexieAccount); + } else { + unmodified.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); + for (const sqliteDid of sqliteDids) { + const dexieAccount = dexieAccounts.find((a) => a.did === sqliteDid); if (!dexieAccount) { - missing.push(sqliteAccount); + missing.push(sqliteDid); } } - return { added, modified, missing }; + return { added, unmodified, missing }; } /** @@ -688,15 +656,15 @@ function compareAccounts(dexieAccounts: Account[], sqliteAccounts: Account[]) { */ function contactsEqual(contact1: Contact, contact2: Contact): boolean { return ( - contact1.did === contact2.did && - contact1.name === contact2.name && - contact1.notes === contact2.notes && - contact1.profileImageUrl === contact2.profileImageUrl && - contact1.publicKeyBase64 === contact2.publicKeyBase64 && - contact1.nextPubKeyHashB64 === contact2.nextPubKeyHashB64 && - contact1.seesMe === contact2.seesMe && - contact1.registered === contact2.registered && - JSON.stringify(contact1.contactMethods) === + contact1.did == contact2.did && + contact1.name == contact2.name && + contact1.notes == contact2.notes && + contact1.profileImageUrl == contact2.profileImageUrl && + contact1.publicKeyBase64 == contact2.publicKeyBase64 && + contact1.nextPubKeyHashB64 == contact2.nextPubKeyHashB64 && + contact1.seesMe == contact2.seesMe && + contact1.registered == contact2.registered && + JSON.stringify(contact1.contactMethods) == JSON.stringify(contact2.contactMethods) ); } @@ -727,38 +695,38 @@ function contactsEqual(contact1: Contact, contact2: Contact): boolean { */ function settingsEqual(settings1: Settings, settings2: Settings): boolean { return ( - settings1.id === settings2.id && - settings1.accountDid === settings2.accountDid && - settings1.activeDid === settings2.activeDid && - settings1.apiServer === settings2.apiServer && - settings1.filterFeedByNearby === settings2.filterFeedByNearby && - settings1.filterFeedByVisible === settings2.filterFeedByVisible && - settings1.finishedOnboarding === settings2.finishedOnboarding && - settings1.firstName === settings2.firstName && - settings1.hideRegisterPromptOnNewContact === + settings1.id == settings2.id && + settings1.accountDid == settings2.accountDid && + settings1.activeDid == settings2.activeDid && + settings1.apiServer == settings2.apiServer && + settings1.filterFeedByNearby == settings2.filterFeedByNearby && + settings1.filterFeedByVisible == settings2.filterFeedByVisible && + settings1.finishedOnboarding == settings2.finishedOnboarding && + settings1.firstName == settings2.firstName && + settings1.hideRegisterPromptOnNewContact == settings2.hideRegisterPromptOnNewContact && - settings1.isRegistered === settings2.isRegistered && - settings1.lastName === settings2.lastName && - settings1.lastAckedOfferToUserJwtId === + settings1.isRegistered == settings2.isRegistered && + settings1.lastName == settings2.lastName && + settings1.lastAckedOfferToUserJwtId == settings2.lastAckedOfferToUserJwtId && - settings1.lastAckedOfferToUserProjectsJwtId === + settings1.lastAckedOfferToUserProjectsJwtId == settings2.lastAckedOfferToUserProjectsJwtId && - settings1.lastNotifiedClaimId === settings2.lastNotifiedClaimId && - settings1.lastViewedClaimId === settings2.lastViewedClaimId && - settings1.notifyingNewActivityTime === settings2.notifyingNewActivityTime && - settings1.notifyingReminderMessage === settings2.notifyingReminderMessage && - settings1.notifyingReminderTime === settings2.notifyingReminderTime && - settings1.partnerApiServer === settings2.partnerApiServer && - settings1.passkeyExpirationMinutes === settings2.passkeyExpirationMinutes && - settings1.profileImageUrl === settings2.profileImageUrl && - settings1.showContactGivesInline === settings2.showContactGivesInline && - settings1.showGeneralAdvanced === settings2.showGeneralAdvanced && - settings1.showShortcutBvc === settings2.showShortcutBvc && - settings1.vapid === settings2.vapid && - settings1.warnIfProdServer === settings2.warnIfProdServer && - settings1.warnIfTestServer === settings2.warnIfTestServer && - settings1.webPushServer === settings2.webPushServer && - JSON.stringify(settings1.searchBoxes) === + settings1.lastNotifiedClaimId == settings2.lastNotifiedClaimId && + settings1.lastViewedClaimId == settings2.lastViewedClaimId && + settings1.notifyingNewActivityTime == settings2.notifyingNewActivityTime && + settings1.notifyingReminderMessage == settings2.notifyingReminderMessage && + settings1.notifyingReminderTime == settings2.notifyingReminderTime && + settings1.partnerApiServer == settings2.partnerApiServer && + settings1.passkeyExpirationMinutes == settings2.passkeyExpirationMinutes && + settings1.profileImageUrl == settings2.profileImageUrl && + settings1.showContactGivesInline == settings2.showContactGivesInline && + settings1.showGeneralAdvanced == settings2.showGeneralAdvanced && + settings1.showShortcutBvc == settings2.showShortcutBvc && + settings1.vapid == settings2.vapid && + settings1.warnIfProdServer == settings2.warnIfProdServer && + settings1.warnIfTestServer == settings2.warnIfTestServer && + settings1.webPushServer == settings2.webPushServer && + JSON.stringify(settings1.searchBoxes) == JSON.stringify(settings2.searchBoxes) ); } @@ -830,23 +798,25 @@ export function generateComparisonYaml(comparison: DataComparison): string { dexieSettings: comparison.dexieSettings.length, sqliteSettings: comparison.sqliteSettings.filter(s => s.accountDid || s.activeDid).length, dexieAccounts: comparison.dexieAccounts.length, - sqliteAccounts: comparison.sqliteAccounts.filter(a => a.did).length, + sqliteAccounts: comparison.sqliteAccounts.filter(a => a).length, }, differences: { contacts: { added: comparison.differences.contacts.added.length, modified: comparison.differences.contacts.modified.length, + unmodified: comparison.differences.contacts.unmodified.length, missing: comparison.differences.contacts.missing.filter(c => c.did).length, }, settings: { added: comparison.differences.settings.added.length, modified: comparison.differences.settings.modified.length, + unmodified: comparison.differences.settings.unmodified.length, missing: comparison.differences.settings.missing.filter(s => s.accountDid || s.activeDid).length, }, accounts: { added: comparison.differences.accounts.added.length, - modified: comparison.differences.accounts.modified.length, - missing: comparison.differences.accounts.missing.filter(a => a.did).length, + unmodified: comparison.differences.accounts.unmodified.length, + missing: comparison.differences.accounts.missing.filter(a => a).length, }, }, details: { @@ -888,15 +858,9 @@ export function generateComparisonYaml(comparison: DataComparison): string { hasIdentity: !!a.identity, hasMnemonic: !!a.mnemonic, })), - sqlite: comparison.sqliteAccounts - .filter(a => a.did) - .map((a) => ({ - id: a.id, - did: a.did, - dateCreated: a.dateCreated, - hasIdentity: !!a.identity, - hasMnemonic: !!a.mnemonic, - })), + sqlite: comparison.sqliteAccounts.map((a) => ({ + did: a, + })), }, }, }; diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index 8565f30e..8fb907fd 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -74,7 +74,7 @@

- Here is your seed. Write it down! + Here is your seed for account {{ downloadMnemonicAddress?.substring('did:ethr:0x'.length).substring(0, 3) }} -- write it down!
{{ downloadMnemonic }}

@@ -506,15 +506,15 @@ >
ModifyUnmodified
{{ - comparison.differences.accounts.modified.length + comparison.differences.accounts.unmodified.length }}
@@ -559,48 +559,44 @@ - +

- Modify Accounts ({{ comparison.differences.accounts.modified.length }}): + Unmodified Accounts ({{ comparison.differences.accounts.unmodified.length }}):

-
-
-
ID: {{ account.id }}
-
{{ account.did }}
-
Created: {{ account.dateCreated }}
-
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
-
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}
-
+
+
+
+
ID: {{ account.id }}
+
{{ account.did }}
+
Created: {{ account.dateCreated }}
+
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
+
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}

- Keep Accounts ({{ comparison.differences.accounts.missing.filter(a => a.did).length }}): + Keep Accounts ({{ comparison.differences.accounts.missing.filter(a => a).length }}):

-
ID: {{ account.id }}
-
{{ account.did }}
-
Created: {{ account.dateCreated }}
-
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
-
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}
+
{{ did }}
@@ -647,6 +643,23 @@ }}
+
+
+ + Unmodified +
+ {{ + comparison.differences.settings.unmodified.length + }} +
+
@@ -707,6 +720,27 @@
+ +
+

+ Unmodified Settings ({{ comparison.differences.settings.unmodified.length }}): +

+
+
+
+
{{ getSettingDisplayName(setting) }}
+
ID: {{ setting.id }}
+
Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}
+
+
+
+
+
+ + Unmodified +
+ {{ + comparison.differences.contacts.unmodified.length + }} +
+
@@ -830,6 +881,27 @@
+ +
+

+ Unmodified Contacts ({{ comparison.differences.contacts.unmodified.length }}): +

+
+
+
+
{{ contact.name || '' }}
+
{{ contact.did }}
+
{{ contact.contactMethods?.length || 0 }} contact methods
+
+
+