forked from trent_larson/crowd-funder-for-time-pwa
IndexedDB migration: implement the migrations differently
This commit is contained in:
@@ -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(", ")}`,
|
||||
|
||||
Reference in New Issue
Block a user