forked from trent_larson/crowd-funder-for-time-pwa
IndexedDB migration: implement the migrations differently
This commit is contained in:
@@ -227,10 +227,28 @@ export async function logConsoleAndDb(
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL INSERT statement and parameters from a model object.
|
||||
* @param model The model object containing fields to update
|
||||
* @param tableName The name of the table to update
|
||||
* @returns Object containing the SQL statement and parameters array
|
||||
* 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']
|
||||
* ```
|
||||
*/
|
||||
export function generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
@@ -248,12 +266,30 @@ export function generateInsertStatement(
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL UPDATE statement and parameters from a model object.
|
||||
* @param model The model object containing fields to update
|
||||
* @param tableName The name of the table to update
|
||||
* @param whereClause The WHERE clause for the update (e.g. "id = ?")
|
||||
* @param whereParams Parameters for the WHERE clause
|
||||
* @returns Object containing the SQL statement and parameters array
|
||||
* 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']
|
||||
* ```
|
||||
*/
|
||||
export function generateUpdateStatement(
|
||||
model: Record<string, unknown>,
|
||||
|
||||
@@ -1021,12 +1021,12 @@ export async function importFromMnemonic(
|
||||
// Create new identifier
|
||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||
|
||||
// Handle database operations
|
||||
const accountsDB = await accountsDBPromise;
|
||||
// Handle erasures
|
||||
if (shouldErase) {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec("DELETE FROM accounts");
|
||||
if (USE_DEXIE_DB) {
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.accounts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(", ")}`,
|
||||
|
||||
@@ -566,7 +566,7 @@ export default class ContactImportView extends Vue {
|
||||
this.checkingImports = true;
|
||||
|
||||
try {
|
||||
const jwt: string = getContactJwtFromJwtUrl(jwtInput);
|
||||
const jwt: string = getContactJwtFromJwtUrl(jwtInput) || "";
|
||||
const payload = decodeEndorserJwt(jwt).payload;
|
||||
|
||||
if (Array.isArray(payload.contacts)) {
|
||||
|
||||
@@ -169,11 +169,64 @@
|
||||
icon-name="check"
|
||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||
/>
|
||||
Migrate All (Recommended)
|
||||
Migrate All
|
||||
</button>
|
||||
|
||||
<div class="w-full border-t border-gray-200 my-4"></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>
|
||||
|
||||
<div class="w-full border-t border-gray-200 my-4"></div>
|
||||
|
||||
<button
|
||||
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-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"
|
||||
@@ -194,25 +247,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
:disabled="isLoading || !downloadSettingsContactsBlob || !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 || !downloadSettingsContactsBlob || !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 || !downloadMnemonic || !comparison"
|
||||
: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-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"
|
||||
>
|
||||
@@ -221,12 +256,21 @@
|
||||
</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"
|
||||
: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-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="download" svg-class="-ml-1 mr-3 h-5 w-5" />
|
||||
Export Comparison
|
||||
<IconRenderer icon-name="settings" svg-class="-ml-1 mr-3 h-5 w-5" />
|
||||
Migrate Settings
|
||||
</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-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>
|
||||
</div>
|
||||
|
||||
@@ -287,49 +331,6 @@
|
||||
</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 -->
|
||||
@@ -945,6 +946,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import IconRenderer from "../components/IconRenderer.vue";
|
||||
import {
|
||||
@@ -989,6 +991,8 @@ import { logger } from "../utils/logger";
|
||||
},
|
||||
})
|
||||
export default class DatabaseMigration extends Vue {
|
||||
$router!: Router;
|
||||
|
||||
// Component state
|
||||
private comparison: DataComparison | null = null;
|
||||
private cannotfindMainAccount = false;
|
||||
@@ -1148,6 +1152,7 @@ export default class DatabaseMigration extends Vue {
|
||||
if (result.warnings.length > 0) {
|
||||
this.successMessage += ` ${result.warnings.length} warnings.`;
|
||||
}
|
||||
this.successMessage += " Now finish by migrating contacts.";
|
||||
logger.info(
|
||||
"[DatabaseMigration] Complete migration successful",
|
||||
result,
|
||||
@@ -1211,39 +1216,15 @@ export default class DatabaseMigration extends Vue {
|
||||
* @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,
|
||||
);
|
||||
|
||||
// Refresh comparison data after successful migration
|
||||
this.comparison = await compareDatabases();
|
||||
} 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("");
|
||||
}
|
||||
// load all contacts from indexedDB
|
||||
const dexieContacts = await getDexieContacts();
|
||||
// now reroute to the contact import view with query parameter of contacts
|
||||
this.$router.push({
|
||||
name: "contact-import",
|
||||
query: {
|
||||
contacts: JSON.stringify(dexieContacts),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1306,9 +1287,7 @@ export default class DatabaseMigration extends Vue {
|
||||
this.clearMessages();
|
||||
|
||||
try {
|
||||
const result: MigrationResult = await migrateAccounts(
|
||||
this.overwriteExisting,
|
||||
);
|
||||
const result: MigrationResult = await migrateAccounts();
|
||||
|
||||
if (result.success) {
|
||||
this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`;
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<div v-if="numAccounts == 1" class="mt-4">
|
||||
<input v-model="shouldErase" type="checkbox" class="mr-2" />
|
||||
<label>Erase the previous identifier.</label>
|
||||
<label>Erase previous identifiers.</label>
|
||||
</div>
|
||||
|
||||
<div v-if="isNotProdServer()" class="mt-4 text-blue-500">
|
||||
|
||||
Reference in New Issue
Block a user