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.
|
* Generates 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
|
* This helper function creates a parameterized SQL INSERT statement
|
||||||
* @returns Object containing the SQL statement and parameters array
|
* 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(
|
export function generateInsertStatement(
|
||||||
model: Record<string, unknown>,
|
model: Record<string, unknown>,
|
||||||
@@ -248,12 +266,30 @@ export function generateInsertStatement(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an SQL UPDATE statement and parameters from a model object.
|
* Generates 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
|
* This helper function creates a parameterized SQL UPDATE statement
|
||||||
* @param whereClause The WHERE clause for the update (e.g. "id = ?")
|
* from a JavaScript object. It filters out undefined values and
|
||||||
* @param whereParams Parameters for the WHERE clause
|
* creates the appropriate SQL syntax with placeholders.
|
||||||
* @returns Object containing the SQL statement and parameters array
|
*
|
||||||
|
* 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(
|
export function generateUpdateStatement(
|
||||||
model: Record<string, unknown>,
|
model: Record<string, unknown>,
|
||||||
|
|||||||
@@ -1021,12 +1021,12 @@ export async function importFromMnemonic(
|
|||||||
// Create new identifier
|
// Create new identifier
|
||||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||||
|
|
||||||
// Handle database operations
|
// Handle erasures
|
||||||
const accountsDB = await accountsDBPromise;
|
|
||||||
if (shouldErase) {
|
if (shouldErase) {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
await platformService.dbExec("DELETE FROM accounts");
|
await platformService.dbExec("DELETE FROM accounts");
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
|
const accountsDB = await accountsDBPromise;
|
||||||
await accountsDB.accounts.clear();
|
await accountsDB.accounts.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,11 @@ import "dexie-export-import";
|
|||||||
import { PlatformServiceFactory } from "./PlatformServiceFactory";
|
import { PlatformServiceFactory } from "./PlatformServiceFactory";
|
||||||
import { db, accountsDBPromise } 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, MASTER_SETTINGS_KEY, SettingsWithJsonStrings, BoundingBox } from "../db/tables/settings";
|
import { Settings, MASTER_SETTINGS_KEY, BoundingBox } from "../db/tables/settings";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account } from "../db/tables/accounts";
|
||||||
import { logger } from "../utils/logger";
|
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 { importFromMnemonic } from "../libs/util";
|
||||||
import { IIdentifier } from "@veramo/core";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for data comparison results between Dexie and SQLite databases
|
* Interface for data comparison results between Dexie and SQLite databases
|
||||||
@@ -1028,228 +1027,47 @@ export async function migrateSettings(
|
|||||||
try {
|
try {
|
||||||
const dexieSettings = await getDexieSettings();
|
const dexieSettings = await getDexieSettings();
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
// loop through dexieSettings,
|
||||||
// Group settings by DID to handle duplicates
|
// load the one with the matching accountDid from sqlite,
|
||||||
const settingsByDid = new Map<string, {
|
// and if one doesn't exist then insert it,
|
||||||
master?: Settings;
|
// otherwise, update the fields
|
||||||
account?: Settings;
|
dexieSettings.forEach(async (setting) => {
|
||||||
}>();
|
const sqliteSettingRaw = await platformService.dbQuery(
|
||||||
|
"SELECT * FROM settings WHERE accountDid = ?",
|
||||||
// Organize settings by DID
|
[setting.accountDid]
|
||||||
dexieSettings.forEach(setting => {
|
);
|
||||||
const isMasterSetting = setting.id === MASTER_SETTINGS_KEY;
|
if (sqliteSettingRaw?.values?.length) {
|
||||||
const did = isMasterSetting ? setting.activeDid : setting.accountDid;
|
// should cover the master settings, were accountDid is null
|
||||||
|
const sqliteSetting = mapColumnsToValues(sqliteSettingRaw.columns, sqliteSettingRaw.values) as unknown as Settings;
|
||||||
if (!did) {
|
let conditional: string;
|
||||||
result.warnings.push(`Setting ${setting.id} has no DID, skipping`);
|
let preparams: unknown[];
|
||||||
return;
|
if (!setting.accountDid) {
|
||||||
}
|
conditional = "accountDid is null";
|
||||||
|
preparams = [];
|
||||||
if (!settingsByDid.has(did)) {
|
} else {
|
||||||
settingsByDid.set(did, {});
|
conditional = "accountDid = ?";
|
||||||
}
|
preparams = [setting.accountDid];
|
||||||
|
}
|
||||||
const didSettings = settingsByDid.get(did)!;
|
const { sql, params } = generateUpdateStatement(
|
||||||
if (isMasterSetting) {
|
sqliteSetting as unknown as Record<string, unknown>,
|
||||||
didSettings.master = setting;
|
"settings",
|
||||||
logger.info("[MigrationService] Found master settings", {
|
conditional,
|
||||||
did,
|
preparams
|
||||||
id: setting.id,
|
);
|
||||||
firstName: setting.firstName,
|
await platformService.dbExec(sql, params);
|
||||||
isRegistered: setting.isRegistered,
|
result.settingsMigrated++;
|
||||||
profileImageUrl: setting.profileImageUrl,
|
|
||||||
showShortcutBvc: setting.showShortcutBvc,
|
|
||||||
searchBoxes: setting.searchBoxes
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
didSettings.account = setting;
|
// insert new setting
|
||||||
logger.info("[MigrationService] Found account settings", {
|
delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case)
|
||||||
did,
|
const { sql, params } = generateInsertStatement(
|
||||||
id: setting.id,
|
setting as unknown as Record<string, unknown>,
|
||||||
firstName: setting.firstName,
|
"settings"
|
||||||
isRegistered: setting.isRegistered,
|
);
|
||||||
profileImageUrl: setting.profileImageUrl,
|
await platformService.dbExec(sql, params);
|
||||||
showShortcutBvc: setting.showShortcutBvc,
|
result.settingsMigrated++;
|
||||||
searchBoxes: setting.searchBoxes
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = `Settings migration failed: ${error}`;
|
const errorMessage = `Settings migration failed: ${error}`;
|
||||||
@@ -1277,7 +1095,6 @@ export async function migrateSettings(
|
|||||||
*
|
*
|
||||||
* @async
|
* @async
|
||||||
* @function migrateAccounts
|
* @function migrateAccounts
|
||||||
* @param {boolean} [overwriteExisting=false] - Whether to overwrite existing accounts in SQLite
|
|
||||||
* @returns {Promise<MigrationResult>} Detailed results of the migration operation
|
* @returns {Promise<MigrationResult>} Detailed results of the migration operation
|
||||||
* @throws {Error} If the migration process fails completely
|
* @throws {Error} If the migration process fails completely
|
||||||
* @example
|
* @example
|
||||||
@@ -1294,12 +1111,8 @@ export async function migrateSettings(
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function migrateAccounts(
|
export async function migrateAccounts(): Promise<MigrationResult> {
|
||||||
overwriteExisting: boolean = false,
|
logger.info("[MigrationService] Starting account migration");
|
||||||
): Promise<MigrationResult> {
|
|
||||||
logger.info("[MigrationService] Starting account migration", {
|
|
||||||
overwriteExisting,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: MigrationResult = {
|
const result: MigrationResult = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -1335,67 +1148,17 @@ export async function migrateAccounts(
|
|||||||
[did]
|
[did]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingResult?.values?.length && !overwriteExisting) {
|
if (existingResult?.values?.length) {
|
||||||
result.warnings.push(`Account with DID ${did} already exists, skipping`);
|
result.warnings.push(`Account with DID ${did} already exists, skipping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (account.mnemonic) {
|
||||||
// Map Dexie fields to SQLite fields
|
await importFromMnemonic(account.mnemonic, account.derivationPath);
|
||||||
const accountData = {
|
result.accountsMigrated++;
|
||||||
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
|
|
||||||
]
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await platformService.dbQuery(
|
result.errors.push(`Account with DID ${did} has no mnemonic, skipping`);
|
||||||
`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.accountsMigrated++;
|
|
||||||
logger.info("[MigrationService] Successfully migrated account", {
|
logger.info("[MigrationService] Successfully migrated account", {
|
||||||
did,
|
did,
|
||||||
dateCreated: account.dateCreated
|
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
|
* Migrates all data from Dexie to SQLite in the proper order
|
||||||
*
|
*
|
||||||
@@ -1551,7 +1221,7 @@ export async function migrateAll(
|
|||||||
|
|
||||||
// Step 1: Migrate Accounts (foundational)
|
// Step 1: Migrate Accounts (foundational)
|
||||||
logger.info("[MigrationService] Step 1: Migrating accounts...");
|
logger.info("[MigrationService] Step 1: Migrating accounts...");
|
||||||
const accountsResult = await migrateAccounts(overwriteExisting);
|
const accountsResult = await migrateAccounts();
|
||||||
if (!accountsResult.success) {
|
if (!accountsResult.success) {
|
||||||
result.errors.push(
|
result.errors.push(
|
||||||
`Account migration failed: ${accountsResult.errors.join(", ")}`,
|
`Account migration failed: ${accountsResult.errors.join(", ")}`,
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ export default class ContactImportView extends Vue {
|
|||||||
this.checkingImports = true;
|
this.checkingImports = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jwt: string = getContactJwtFromJwtUrl(jwtInput);
|
const jwt: string = getContactJwtFromJwtUrl(jwtInput) || "";
|
||||||
const payload = decodeEndorserJwt(jwt).payload;
|
const payload = decodeEndorserJwt(jwt).payload;
|
||||||
|
|
||||||
if (Array.isArray(payload.contacts)) {
|
if (Array.isArray(payload.contacts)) {
|
||||||
|
|||||||
@@ -169,11 +169,64 @@
|
|||||||
icon-name="check"
|
icon-name="check"
|
||||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||||
/>
|
/>
|
||||||
Migrate All (Recommended)
|
Migrate All
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="w-full border-t border-gray-200 my-4"></div>
|
<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
|
<button
|
||||||
:disabled="isLoading"
|
: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"
|
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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
:disabled="isLoading || !downloadSettingsContactsBlob || !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-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"
|
|
||||||
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"
|
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"
|
@click="migrateAccounts"
|
||||||
>
|
>
|
||||||
@@ -221,12 +256,21 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
:disabled="!comparison"
|
:disabled="isLoading"
|
||||||
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"
|
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="exportComparison"
|
@click="migrateSettings"
|
||||||
>
|
>
|
||||||
<IconRenderer icon-name="download" svg-class="-ml-1 mr-3 h-5 w-5" />
|
<IconRenderer icon-name="settings" svg-class="-ml-1 mr-3 h-5 w-5" />
|
||||||
Export Comparison
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -287,49 +331,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Comparison Results -->
|
||||||
<div v-if="comparison" class="space-y-6">
|
<div v-if="comparison" class="space-y-6">
|
||||||
<!-- Summary Cards -->
|
<!-- Summary Cards -->
|
||||||
@@ -945,6 +946,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import IconRenderer from "../components/IconRenderer.vue";
|
import IconRenderer from "../components/IconRenderer.vue";
|
||||||
import {
|
import {
|
||||||
@@ -989,6 +991,8 @@ import { logger } from "../utils/logger";
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DatabaseMigration extends Vue {
|
export default class DatabaseMigration extends Vue {
|
||||||
|
$router!: Router;
|
||||||
|
|
||||||
// Component state
|
// Component state
|
||||||
private comparison: DataComparison | null = null;
|
private comparison: DataComparison | null = null;
|
||||||
private cannotfindMainAccount = false;
|
private cannotfindMainAccount = false;
|
||||||
@@ -1148,6 +1152,7 @@ export default class DatabaseMigration extends Vue {
|
|||||||
if (result.warnings.length > 0) {
|
if (result.warnings.length > 0) {
|
||||||
this.successMessage += ` ${result.warnings.length} warnings.`;
|
this.successMessage += ` ${result.warnings.length} warnings.`;
|
||||||
}
|
}
|
||||||
|
this.successMessage += " Now finish by migrating contacts.";
|
||||||
logger.info(
|
logger.info(
|
||||||
"[DatabaseMigration] Complete migration successful",
|
"[DatabaseMigration] Complete migration successful",
|
||||||
result,
|
result,
|
||||||
@@ -1211,39 +1216,15 @@ export default class DatabaseMigration extends Vue {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async migrateContacts(): Promise<void> {
|
async migrateContacts(): Promise<void> {
|
||||||
this.setLoading("Migrating contacts...");
|
// load all contacts from indexedDB
|
||||||
this.clearMessages();
|
const dexieContacts = await getDexieContacts();
|
||||||
|
// now reroute to the contact import view with query parameter of contacts
|
||||||
try {
|
this.$router.push({
|
||||||
const result: MigrationResult = await migrateContacts(
|
name: "contact-import",
|
||||||
this.overwriteExisting,
|
query: {
|
||||||
);
|
contacts: JSON.stringify(dexieContacts),
|
||||||
|
},
|
||||||
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("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1306,9 +1287,7 @@ export default class DatabaseMigration extends Vue {
|
|||||||
this.clearMessages();
|
this.clearMessages();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result: MigrationResult = await migrateAccounts(
|
const result: MigrationResult = await migrateAccounts();
|
||||||
this.overwriteExisting,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`;
|
this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
<div v-if="numAccounts == 1" class="mt-4">
|
<div v-if="numAccounts == 1" class="mt-4">
|
||||||
<input v-model="shouldErase" type="checkbox" class="mr-2" />
|
<input v-model="shouldErase" type="checkbox" class="mr-2" />
|
||||||
<label>Erase the previous identifier.</label>
|
<label>Erase previous identifiers.</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isNotProdServer()" class="mt-4 text-blue-500">
|
<div v-if="isNotProdServer()" class="mt-4 text-blue-500">
|
||||||
|
|||||||
Reference in New Issue
Block a user