IndexedDB migration: implement the migrations differently

This commit is contained in:
2025-06-19 17:26:30 -06:00
parent 4e215914a3
commit 37f2ba1382
6 changed files with 181 additions and 496 deletions

View File

@@ -26,12 +26,11 @@ 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, SettingsWithJsonStrings, BoundingBox } from "../db/tables/settings";
import { Account, AccountEncrypted } from "../db/tables/accounts";
import { Settings, MASTER_SETTINGS_KEY, BoundingBox } from "../db/tables/settings";
import { Account } from "../db/tables/accounts";
import { logger } from "../utils/logger";
import { mapColumnsToValues, parseJsonField } from "../db/databaseUtil";
import { mapColumnsToValues, parseJsonField, generateUpdateStatement, generateInsertStatement } from "../db/databaseUtil";
import { importFromMnemonic } from "../libs/util";
import { IIdentifier } from "@veramo/core";
/**
* Interface for data comparison results between Dexie and SQLite databases
@@ -1028,228 +1027,47 @@ export async function migrateSettings(
try {
const dexieSettings = await getDexieSettings();
const platformService = PlatformServiceFactory.getInstance();
// Group settings by DID to handle duplicates
const settingsByDid = new Map<string, {
master?: Settings;
account?: Settings;
}>();
// Organize settings by DID
dexieSettings.forEach(setting => {
const isMasterSetting = setting.id === MASTER_SETTINGS_KEY;
const did = isMasterSetting ? setting.activeDid : setting.accountDid;
if (!did) {
result.warnings.push(`Setting ${setting.id} has no DID, skipping`);
return;
}
if (!settingsByDid.has(did)) {
settingsByDid.set(did, {});
}
const didSettings = settingsByDid.get(did)!;
if (isMasterSetting) {
didSettings.master = setting;
logger.info("[MigrationService] Found master settings", {
did,
id: setting.id,
firstName: setting.firstName,
isRegistered: setting.isRegistered,
profileImageUrl: setting.profileImageUrl,
showShortcutBvc: setting.showShortcutBvc,
searchBoxes: setting.searchBoxes
});
// loop through dexieSettings,
// load the one with the matching accountDid from sqlite,
// and if one doesn't exist then insert it,
// otherwise, update the fields
dexieSettings.forEach(async (setting) => {
const sqliteSettingRaw = await platformService.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[setting.accountDid]
);
if (sqliteSettingRaw?.values?.length) {
// should cover the master settings, were accountDid is null
const sqliteSetting = mapColumnsToValues(sqliteSettingRaw.columns, sqliteSettingRaw.values) as unknown as Settings;
let conditional: string;
let preparams: unknown[];
if (!setting.accountDid) {
conditional = "accountDid is null";
preparams = [];
} else {
conditional = "accountDid = ?";
preparams = [setting.accountDid];
}
const { sql, params } = generateUpdateStatement(
sqliteSetting as unknown as Record<string, unknown>,
"settings",
conditional,
preparams
);
await platformService.dbExec(sql, params);
result.settingsMigrated++;
} else {
didSettings.account = setting;
logger.info("[MigrationService] Found account settings", {
did,
id: setting.id,
firstName: setting.firstName,
isRegistered: setting.isRegistered,
profileImageUrl: setting.profileImageUrl,
showShortcutBvc: setting.showShortcutBvc,
searchBoxes: setting.searchBoxes
});
// insert new setting
delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case)
const { sql, params } = generateInsertStatement(
setting as unknown as Record<string, unknown>,
"settings"
);
await platformService.dbExec(sql, params);
result.settingsMigrated++;
}
});
// Process each unique DID's settings
for (const [did, didSettings] of settingsByDid.entries()) {
try {
// Process master settings
if (didSettings.master) {
const masterData = {
id: MASTER_SETTINGS_KEY,
activeDid: did,
accountDid: "", // Empty for master settings
apiServer: didSettings.master.apiServer || "",
filterFeedByNearby: didSettings.master.filterFeedByNearby || false,
filterFeedByVisible: didSettings.master.filterFeedByVisible || false,
finishedOnboarding: didSettings.master.finishedOnboarding || false,
firstName: didSettings.master.firstName || "",
hideRegisterPromptOnNewContact: didSettings.master.hideRegisterPromptOnNewContact || false,
isRegistered: didSettings.master.isRegistered || false,
lastName: didSettings.master.lastName || "",
profileImageUrl: didSettings.master.profileImageUrl || "",
searchBoxes: didSettings.master.searchBoxes || [],
showShortcutBvc: didSettings.master.showShortcutBvc || false
};
// Check if master setting exists
const existingMaster = await platformService.dbQuery(
"SELECT id FROM settings WHERE id = ? AND activeDid = ? AND accountDid = ''",
[MASTER_SETTINGS_KEY, did]
);
if (existingMaster?.values?.length) {
logger.info("[MigrationService] Updating master settings", { did, masterData });
await platformService.dbQuery(
`UPDATE settings SET
activeDid = ?,
accountDid = ?,
firstName = ?,
isRegistered = ?,
profileImageUrl = ?,
showShortcutBvc = ?,
searchBoxes = ?
WHERE id = ?`,
[
masterData.activeDid,
masterData.accountDid,
masterData.firstName,
masterData.isRegistered,
masterData.profileImageUrl,
masterData.showShortcutBvc,
JSON.stringify(masterData.searchBoxes),
MASTER_SETTINGS_KEY
]
);
} else {
logger.info("[MigrationService] Inserting master settings", { did, masterData });
await platformService.dbQuery(
`INSERT INTO settings (
id,
activeDid,
accountDid,
firstName,
isRegistered,
profileImageUrl,
showShortcutBvc,
searchBoxes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
MASTER_SETTINGS_KEY,
masterData.activeDid,
masterData.accountDid,
masterData.firstName,
masterData.isRegistered,
masterData.profileImageUrl,
masterData.showShortcutBvc,
JSON.stringify(masterData.searchBoxes)
]
);
}
result.settingsMigrated++;
}
// Process account settings
if (didSettings.account) {
const accountData = {
id: 2, // Account settings always use id 2
activeDid: "", // Empty for account settings
accountDid: did,
apiServer: didSettings.account.apiServer || "",
filterFeedByNearby: didSettings.account.filterFeedByNearby || false,
filterFeedByVisible: didSettings.account.filterFeedByVisible || false,
finishedOnboarding: didSettings.account.finishedOnboarding || false,
firstName: didSettings.account.firstName || "",
hideRegisterPromptOnNewContact: didSettings.account.hideRegisterPromptOnNewContact || false,
isRegistered: didSettings.account.isRegistered || false,
lastName: didSettings.account.lastName || "",
profileImageUrl: didSettings.account.profileImageUrl || "",
searchBoxes: didSettings.account.searchBoxes || [],
showShortcutBvc: didSettings.account.showShortcutBvc || false
};
// Check if account setting exists
const existingAccount = await platformService.dbQuery(
"SELECT id FROM settings WHERE id = ? AND accountDid = ? AND activeDid = ''",
[2, did]
);
if (existingAccount?.values?.length) {
logger.info("[MigrationService] Updating account settings", { did, accountData });
await platformService.dbQuery(
`UPDATE settings SET
activeDid = ?,
accountDid = ?,
firstName = ?,
isRegistered = ?,
profileImageUrl = ?,
showShortcutBvc = ?,
searchBoxes = ?
WHERE id = ?`,
[
accountData.activeDid,
accountData.accountDid,
accountData.firstName,
accountData.isRegistered,
accountData.profileImageUrl,
accountData.showShortcutBvc,
JSON.stringify(accountData.searchBoxes),
2
]
);
} else {
logger.info("[MigrationService] Inserting account settings", { did, accountData });
await platformService.dbQuery(
`INSERT INTO settings (
id,
activeDid,
accountDid,
firstName,
isRegistered,
profileImageUrl,
showShortcutBvc,
searchBoxes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
2,
accountData.activeDid,
accountData.accountDid,
accountData.firstName,
accountData.isRegistered,
accountData.profileImageUrl,
accountData.showShortcutBvc,
JSON.stringify(accountData.searchBoxes)
]
);
}
result.settingsMigrated++;
}
logger.info("[MigrationService] Successfully migrated settings for DID", {
did,
masterMigrated: !!didSettings.master,
accountMigrated: !!didSettings.account
});
} catch (error) {
const errorMessage = `Failed to migrate settings for DID ${did}: ${error}`;
result.errors.push(errorMessage);
logger.error("[MigrationService] Settings migration failed:", {
error,
did
});
}
}
if (result.errors.length > 0) {
result.success = false;
}
return result;
} catch (error) {
const errorMessage = `Settings migration failed: ${error}`;
@@ -1277,7 +1095,6 @@ export async function migrateSettings(
*
* @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
@@ -1294,12 +1111,8 @@ export async function migrateSettings(
* }
* ```
*/
export async function migrateAccounts(
overwriteExisting: boolean = false,
): Promise<MigrationResult> {
logger.info("[MigrationService] Starting account migration", {
overwriteExisting,
});
export async function migrateAccounts(): Promise<MigrationResult> {
logger.info("[MigrationService] Starting account migration");
const result: MigrationResult = {
success: true,
@@ -1335,67 +1148,17 @@ export async function migrateAccounts(
[did]
);
if (existingResult?.values?.length && !overwriteExisting) {
if (existingResult?.values?.length) {
result.warnings.push(`Account with DID ${did} already exists, skipping`);
continue;
}
// Map Dexie fields to SQLite fields
const accountData = {
did: account.did,
dateCreated: account.dateCreated,
derivationPath: account.derivationPath || "",
identityEncrBase64: account.identity || "",
mnemonicEncrBase64: account.mnemonic || "",
passkeyCredIdHex: account.passkeyCredIdHex || "",
publicKeyHex: account.publicKeyHex || ""
};
// Insert or update the account
if (existingResult?.values?.length) {
await platformService.dbQuery(
`UPDATE accounts SET
dateCreated = ?,
derivationPath = ?,
identityEncrBase64 = ?,
mnemonicEncrBase64 = ?,
passkeyCredIdHex = ?,
publicKeyHex = ?
WHERE did = ?`,
[
accountData.dateCreated,
accountData.derivationPath,
accountData.identityEncrBase64,
accountData.mnemonicEncrBase64,
accountData.passkeyCredIdHex,
accountData.publicKeyHex,
did
]
);
if (account.mnemonic) {
await importFromMnemonic(account.mnemonic, account.derivationPath);
result.accountsMigrated++;
} else {
await platformService.dbQuery(
`INSERT INTO accounts (
did,
dateCreated,
derivationPath,
identityEncrBase64,
mnemonicEncrBase64,
passkeyCredIdHex,
publicKeyHex
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
did,
accountData.dateCreated,
accountData.derivationPath,
accountData.identityEncrBase64,
accountData.mnemonicEncrBase64,
accountData.passkeyCredIdHex,
accountData.publicKeyHex
]
);
result.errors.push(`Account with DID ${did} has no mnemonic, skipping`);
}
result.accountsMigrated++;
logger.info("[MigrationService] Successfully migrated account", {
did,
dateCreated: account.dateCreated
@@ -1424,99 +1187,6 @@ export async function migrateAccounts(
}
}
/**
* Generates SQL INSERT statement and parameters from a model object
*
* This helper function creates a parameterized SQL INSERT statement
* from a JavaScript object. It filters out undefined values and
* creates the appropriate SQL syntax with placeholders.
*
* The function is used internally by the migration functions to
* safely insert data into the SQLite database.
*
* @function generateInsertStatement
* @param {Record<string, unknown>} model - The model object containing fields to insert
* @param {string} tableName - The name of the table to insert into
* @returns {Object} Object containing the SQL statement and parameters array
* @returns {string} returns.sql - The SQL INSERT statement
* @returns {unknown[]} returns.params - Array of parameter values
* @example
* ```typescript
* const contact = { did: 'did:example:123', name: 'John Doe' };
* const { sql, params } = generateInsertStatement(contact, 'contacts');
* // sql: "INSERT INTO contacts (did, name) VALUES (?, ?)"
* // params: ['did:example:123', 'John Doe']
* ```
*/
function generateInsertStatement(
model: Record<string, unknown>,
tableName: string,
): { sql: string; params: unknown[] } {
const columns = Object.keys(model).filter((key) => model[key] !== undefined);
const values = Object.values(model).filter((value) => value !== undefined);
const placeholders = values.map(() => "?").join(", ");
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
return {
sql: insertSql,
params: values,
};
}
/**
* Generates SQL UPDATE statement and parameters from a model object
*
* This helper function creates a parameterized SQL UPDATE statement
* from a JavaScript object. It filters out undefined values and
* creates the appropriate SQL syntax with placeholders.
*
* The function is used internally by the migration functions to
* safely update data in the SQLite database.
*
* @function generateUpdateStatement
* @param {Record<string, unknown>} model - The model object containing fields to update
* @param {string} tableName - The name of the table to update
* @param {string} whereClause - The WHERE clause for the update (e.g. "id = ?")
* @param {unknown[]} [whereParams=[]] - Parameters for the WHERE clause
* @returns {Object} Object containing the SQL statement and parameters array
* @returns {string} returns.sql - The SQL UPDATE statement
* @returns {unknown[]} returns.params - Array of parameter values
* @example
* ```typescript
* const contact = { name: 'Jane Doe' };
* const { sql, params } = generateUpdateStatement(contact, 'contacts', 'did = ?', ['did:example:123']);
* // sql: "UPDATE contacts SET name = ? WHERE did = ?"
* // params: ['Jane Doe', 'did:example:123']
* ```
*/
function generateUpdateStatement(
model: Record<string, unknown>,
tableName: string,
whereClause: string,
whereParams: unknown[] = [],
): { sql: string; params: unknown[] } {
const setClauses: string[] = [];
const params: unknown[] = [];
Object.entries(model).forEach(([key, value]) => {
if (value !== undefined) {
setClauses.push(`${key} = ?`);
params.push(value);
}
});
if (setClauses.length === 0) {
throw new Error("No valid fields to update");
}
const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`;
return {
sql,
params: [...params, ...whereParams],
};
}
/**
* Migrates all data from Dexie to SQLite in the proper order
*
@@ -1551,7 +1221,7 @@ export async function migrateAll(
// Step 1: Migrate Accounts (foundational)
logger.info("[MigrationService] Step 1: Migrating accounts...");
const accountsResult = await migrateAccounts(overwriteExisting);
const accountsResult = await migrateAccounts();
if (!accountsResult.success) {
result.errors.push(
`Account migration failed: ${accountsResult.errors.join(", ")}`,