forked from jsnbuchanan/crowd-funder-for-time-pwa
feat(export): Replace CSV export with standardized JSON format
- Add contactsToExportJson utility function for standardized data export - Replace CSV export with JSON format in DataExportSection - Update file extension and MIME type to application/json - Remove Dexie-specific export logic in favor of unified SQLite/Dexie approach - Update success notifications to reflect JSON format - Add TypeScript interfaces for export data structure This change improves data portability and standardization by: - Using a consistent JSON format for data export/import - Supporting both SQLite and Dexie databases - Including all contact fields in export - Properly handling contactMethods as stringified JSON - Maintaining backward compatibility with existing import tools Security: No sensitive data exposure, maintains existing access controls
This commit is contained in:
109
src/libs/util.ts
109
src/libs/util.ts
@@ -80,7 +80,10 @@ export const UNIT_LONG: Record<string, string> = {
|
||||
};
|
||||
/* eslint-enable prettier/prettier */
|
||||
|
||||
const UNIT_CODES: Record<string, { name: string; faIcon: string; decimals: number }> = {
|
||||
const UNIT_CODES: Record<
|
||||
string,
|
||||
{ name: string; faIcon: string; decimals: number }
|
||||
> = {
|
||||
BTC: {
|
||||
name: "Bitcoin",
|
||||
faIcon: "bitcoin-sign",
|
||||
@@ -558,20 +561,15 @@ export const retrieveAccountMetadata = async (
|
||||
};
|
||||
|
||||
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
||||
console.log("[retrieveAllAccountsMetadata] start");
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const sql = `SELECT * FROM accounts`;
|
||||
console.log("[retrieveAllAccountsMetadata] sql: ", sql);
|
||||
const dbAccounts = await platformService.dbQuery(sql);
|
||||
console.log("[retrieveAllAccountsMetadata] dbAccounts: ", dbAccounts);
|
||||
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
||||
let result = accounts.map((account) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
return metadata as Account;
|
||||
});
|
||||
console.log("[retrieveAllAccountsMetadata] result: ", result);
|
||||
console.log("[retrieveAllAccountsMetadata] USE_DEXIE_DB: ", USE_DEXIE_DB);
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
@@ -582,7 +580,6 @@ export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
||||
return metadata as Account;
|
||||
});
|
||||
}
|
||||
console.log("[retrieveAllAccountsMetadata] end", JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -663,10 +660,6 @@ export async function saveNewIdentity(
|
||||
derivationPath: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log("[saveNewIdentity] identity", identity);
|
||||
console.log("[saveNewIdentity] mnemonic", mnemonic);
|
||||
console.log("[saveNewIdentity] newId", newId);
|
||||
console.log("[saveNewIdentity] derivationPath", derivationPath);
|
||||
// add to the new sql db
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const secrets = await platformService.dbQuery(
|
||||
@@ -685,7 +678,6 @@ export async function saveNewIdentity(
|
||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
console.log("[saveNewIdentity] sql: ", sql);
|
||||
const params = [
|
||||
new Date().toISOString(),
|
||||
derivationPath,
|
||||
@@ -694,7 +686,6 @@ export async function saveNewIdentity(
|
||||
encryptedMnemonicBase64,
|
||||
newId.keys[0].publicKeyHex,
|
||||
];
|
||||
console.log("[saveNewIdentity] params: ", params);
|
||||
await platformService.dbExec(sql, params);
|
||||
await databaseUtil.updateDefaultSettings({ activeDid: newId.did });
|
||||
|
||||
@@ -712,7 +703,6 @@ export async function saveNewIdentity(
|
||||
await updateDefaultSettings({ activeDid: newId.did });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[saveNewIdentity] error: ", error);
|
||||
logger.error("Failed to update default settings:", error);
|
||||
throw new Error(
|
||||
"Failed to set default settings. Please try again or restart the app.",
|
||||
@@ -837,3 +827,94 @@ export const sendTestThroughPushServer = async (
|
||||
logger.log("Got response from web push server:", response);
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Contact object to a CSV line string following the established format.
|
||||
* The format matches CONTACT_CSV_HEADER: "name,did,pubKeyBase64,seesMe,registered,contactMethods"
|
||||
* where contactMethods is stored as a stringified JSON array.
|
||||
*
|
||||
* @param contact - The Contact object to convert
|
||||
* @returns A CSV-formatted string representing the contact
|
||||
* @throws {Error} If the contact object is missing required fields
|
||||
*/
|
||||
export const contactToCsvLine = (contact: Contact): string => {
|
||||
if (!contact.did) {
|
||||
throw new Error("Contact must have a did field");
|
||||
}
|
||||
|
||||
// Escape fields that might contain commas or quotes
|
||||
const escapeField = (field: string | boolean | undefined): string => {
|
||||
if (field === undefined) return "";
|
||||
const str = String(field);
|
||||
if (str.includes(",") || str.includes('"')) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
// Handle contactMethods array by stringifying it
|
||||
const contactMethodsStr = contact.contactMethods
|
||||
? escapeField(JSON.stringify(contact.contactMethods))
|
||||
: "";
|
||||
|
||||
const fields = [
|
||||
escapeField(contact.name),
|
||||
escapeField(contact.did),
|
||||
escapeField(contact.publicKeyBase64),
|
||||
escapeField(contact.seesMe),
|
||||
escapeField(contact.registered),
|
||||
contactMethodsStr,
|
||||
];
|
||||
|
||||
return fields.join(",");
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for the JSON export format of database tables
|
||||
*/
|
||||
export interface TableExportData {
|
||||
tableName: string;
|
||||
inbound: boolean;
|
||||
rows: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the complete database export format
|
||||
*/
|
||||
export interface DatabaseExport {
|
||||
data: {
|
||||
data: Array<TableExportData>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of contacts to the standardized database export JSON format.
|
||||
* This format is used for data migration and backup purposes.
|
||||
*
|
||||
* @param contacts - Array of Contact objects to convert
|
||||
* @returns DatabaseExport object in the standardized format
|
||||
*/
|
||||
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||
// Convert each contact to a plain object and ensure all fields are included
|
||||
const rows = contacts.map(contact => ({
|
||||
did: contact.did,
|
||||
name: contact.name || null,
|
||||
contactMethods: contact.contactMethods ? JSON.stringify(contact.contactMethods) : null,
|
||||
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
||||
notes: contact.notes || null,
|
||||
profileImageUrl: contact.profileImageUrl || null,
|
||||
publicKeyBase64: contact.publicKeyBase64 || null,
|
||||
seesMe: contact.seesMe || false,
|
||||
registered: contact.registered || false
|
||||
}));
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: [{
|
||||
tableName: "contacts",
|
||||
inbound: true,
|
||||
rows
|
||||
}]
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user