forked from trent_larson/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:
@@ -63,13 +63,18 @@ backup and database export, with platform-specific download instructions. * *
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||||
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import { db } from "../db/index";
|
|
||||||
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
import {
|
import {
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} from "../services/PlatformService";
|
} from "../services/PlatformService";
|
||||||
|
import { contactsToExportJson } from "../libs/util";
|
||||||
|
import { db } from "../db/index";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @vue-component
|
* @vue-component
|
||||||
@@ -131,24 +136,25 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
public async exportDatabase() {
|
public async exportDatabase() {
|
||||||
try {
|
try {
|
||||||
if (!USE_DEXIE_DB) {
|
let allContacts: Contact[] = [];
|
||||||
throw new Error("Not implemented");
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
||||||
|
if (result) {
|
||||||
|
allContacts = databaseUtil.mapQueryResultToValues(
|
||||||
|
result,
|
||||||
|
) as unknown as Contact[];
|
||||||
}
|
}
|
||||||
const blob = await db.export({
|
// if (USE_DEXIE_DB) {
|
||||||
prettyJson: true,
|
// await db.open();
|
||||||
transform: (table, value, key) => {
|
// allContacts = await db.contacts.toArray();
|
||||||
if (table === "contacts") {
|
// }
|
||||||
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
|
|
||||||
Object.keys(value).forEach((prop) => {
|
// Convert contacts to export format
|
||||||
if (value[prop] === undefined) {
|
const exportData = contactsToExportJson(allContacts);
|
||||||
delete value[prop];
|
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||||
}
|
const blob = new Blob([jsonStr], { type: "application/json" });
|
||||||
});
|
|
||||||
}
|
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
|
||||||
return { value, key };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup.json`;
|
|
||||||
|
|
||||||
if (this.platformCapabilities.hasFileDownload) {
|
if (this.platformCapabilities.hasFileDownload) {
|
||||||
// Web platform: Use download link
|
// Web platform: Use download link
|
||||||
@@ -160,8 +166,7 @@ export default class DataExportSection extends Vue {
|
|||||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||||
} else if (this.platformCapabilities.hasFileSystem) {
|
} else if (this.platformCapabilities.hasFileSystem) {
|
||||||
// Native platform: Write to app directory
|
// Native platform: Write to app directory
|
||||||
const content = await blob.text();
|
await this.platformService.writeAndShareFile(fileName, jsonStr);
|
||||||
await this.platformService.writeAndShareFile(fileName, content);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("This platform does not support file downloads.");
|
throw new Error("This platform does not support file downloads.");
|
||||||
}
|
}
|
||||||
@@ -172,10 +177,10 @@ export default class DataExportSection extends Vue {
|
|||||||
type: "success",
|
type: "success",
|
||||||
title: "Export Successful",
|
title: "Export Successful",
|
||||||
text: this.platformCapabilities.hasFileDownload
|
text: this.platformCapabilities.hasFileDownload
|
||||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
? "See your downloads directory for the backup. It is in JSON format."
|
||||||
: "You should have been prompted to save your backup file.",
|
: "The backup file has been saved.",
|
||||||
},
|
},
|
||||||
-1,
|
3000,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Export Error:", error);
|
logger.error("Export Error:", error);
|
||||||
|
|||||||
@@ -79,13 +79,9 @@ const DEFAULT_SETTINGS: Settings = {
|
|||||||
|
|
||||||
// retrieves default settings
|
// retrieves default settings
|
||||||
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||||
console.log("[databaseUtil] retrieveSettingsForDefaultAccount");
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
const sql = "SELECT * FROM settings WHERE id = ?";
|
const sql = "SELECT * FROM settings WHERE id = ?";
|
||||||
console.log("[databaseUtil] sql", sql);
|
|
||||||
const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
|
const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
|
||||||
console.log("[databaseUtil] result", JSON.stringify(result, null, 2));
|
|
||||||
console.trace("Trace from [retrieveSettingsForDefaultAccount]");
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return DEFAULT_SETTINGS;
|
return DEFAULT_SETTINGS;
|
||||||
} else {
|
} else {
|
||||||
@@ -103,21 +99,21 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves settings for the active account, merging with default settings
|
* Retrieves settings for the active account, merging with default settings
|
||||||
*
|
*
|
||||||
* @returns Promise<Settings> Combined settings with account-specific overrides
|
* @returns Promise<Settings> Combined settings with account-specific overrides
|
||||||
* @throws Will log specific errors for debugging but returns default settings on failure
|
* @throws Will log specific errors for debugging but returns default settings on failure
|
||||||
*/
|
*/
|
||||||
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||||
logConsoleAndDb("[databaseUtil] Starting settings retrieval for active account");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get default settings first
|
// Get default settings first
|
||||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||||
logConsoleAndDb(`[databaseUtil] Retrieved default settings (hasActiveDid: ${!!defaultSettings.activeDid})`);
|
|
||||||
|
|
||||||
// If no active DID, return defaults
|
// If no active DID, return defaults
|
||||||
if (!defaultSettings.activeDid) {
|
if (!defaultSettings.activeDid) {
|
||||||
logConsoleAndDb("[databaseUtil] No active DID found, returning default settings");
|
logConsoleAndDb(
|
||||||
|
"[databaseUtil] No active DID found, returning default settings",
|
||||||
|
);
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +126,17 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!result?.values?.length) {
|
if (!result?.values?.length) {
|
||||||
logConsoleAndDb(`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`);
|
logConsoleAndDb(
|
||||||
|
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
|
||||||
|
);
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map and filter settings
|
// Map and filter settings
|
||||||
const overrideSettings = mapColumnsToValues(result.columns, result.values)[0] as Settings;
|
const overrideSettings = mapColumnsToValues(
|
||||||
|
result.columns,
|
||||||
|
result.values,
|
||||||
|
)[0] as Settings;
|
||||||
const overrideSettingsFiltered = Object.fromEntries(
|
const overrideSettingsFiltered = Object.fromEntries(
|
||||||
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||||
);
|
);
|
||||||
@@ -151,30 +152,27 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
|
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
// Reset to empty array on parse failure
|
// Reset to empty array on parse failure
|
||||||
settings.searchBoxes = [];
|
settings.searchBoxes = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logConsoleAndDb(
|
|
||||||
`[databaseUtil] Successfully merged settings for ${defaultSettings.activeDid} ` +
|
|
||||||
`(overrides: ${Object.keys(overrideSettingsFiltered).length})`
|
|
||||||
);
|
|
||||||
return settings;
|
return settings;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
|
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
// Return defaults on error
|
// Return defaults on error
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[databaseUtil] Failed to retrieve default settings: ${error}`, true);
|
logConsoleAndDb(
|
||||||
|
`[databaseUtil] Failed to retrieve default settings: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
// Return minimal default settings on complete failure
|
// Return minimal default settings on complete failure
|
||||||
return {
|
return {
|
||||||
id: MASTER_SETTINGS_KEY,
|
id: MASTER_SETTINGS_KEY,
|
||||||
|
|||||||
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 */
|
/* 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: {
|
BTC: {
|
||||||
name: "Bitcoin",
|
name: "Bitcoin",
|
||||||
faIcon: "bitcoin-sign",
|
faIcon: "bitcoin-sign",
|
||||||
@@ -558,20 +561,15 @@ export const retrieveAccountMetadata = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
||||||
console.log("[retrieveAllAccountsMetadata] start");
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const sql = `SELECT * FROM accounts`;
|
const sql = `SELECT * FROM accounts`;
|
||||||
console.log("[retrieveAllAccountsMetadata] sql: ", sql);
|
|
||||||
const dbAccounts = await platformService.dbQuery(sql);
|
const dbAccounts = await platformService.dbQuery(sql);
|
||||||
console.log("[retrieveAllAccountsMetadata] dbAccounts: ", dbAccounts);
|
|
||||||
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
||||||
let result = accounts.map((account) => {
|
let result = accounts.map((account) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { identity, mnemonic, ...metadata } = account;
|
const { identity, mnemonic, ...metadata } = account;
|
||||||
return metadata as Account;
|
return metadata as Account;
|
||||||
});
|
});
|
||||||
console.log("[retrieveAllAccountsMetadata] result: ", result);
|
|
||||||
console.log("[retrieveAllAccountsMetadata] USE_DEXIE_DB: ", USE_DEXIE_DB);
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise;
|
||||||
@@ -582,7 +580,6 @@ export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
|||||||
return metadata as Account;
|
return metadata as Account;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("[retrieveAllAccountsMetadata] end", JSON.stringify(result, null, 2));
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -663,10 +660,6 @@ export async function saveNewIdentity(
|
|||||||
derivationPath: string,
|
derivationPath: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
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
|
// add to the new sql db
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const secrets = await platformService.dbQuery(
|
const secrets = await platformService.dbQuery(
|
||||||
@@ -685,7 +678,6 @@ export async function saveNewIdentity(
|
|||||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||||
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
console.log("[saveNewIdentity] sql: ", sql);
|
|
||||||
const params = [
|
const params = [
|
||||||
new Date().toISOString(),
|
new Date().toISOString(),
|
||||||
derivationPath,
|
derivationPath,
|
||||||
@@ -694,7 +686,6 @@ export async function saveNewIdentity(
|
|||||||
encryptedMnemonicBase64,
|
encryptedMnemonicBase64,
|
||||||
newId.keys[0].publicKeyHex,
|
newId.keys[0].publicKeyHex,
|
||||||
];
|
];
|
||||||
console.log("[saveNewIdentity] params: ", params);
|
|
||||||
await platformService.dbExec(sql, params);
|
await platformService.dbExec(sql, params);
|
||||||
await databaseUtil.updateDefaultSettings({ activeDid: newId.did });
|
await databaseUtil.updateDefaultSettings({ activeDid: newId.did });
|
||||||
|
|
||||||
@@ -712,7 +703,6 @@ export async function saveNewIdentity(
|
|||||||
await updateDefaultSettings({ activeDid: newId.did });
|
await updateDefaultSettings({ activeDid: newId.did });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[saveNewIdentity] error: ", error);
|
|
||||||
logger.error("Failed to update default settings:", error);
|
logger.error("Failed to update default settings:", error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to set default settings. Please try again or restart the app.",
|
"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);
|
logger.log("Got response from web push server:", response);
|
||||||
return 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
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ export class MigrationService {
|
|||||||
sqlQuery: (sql: string) => Promise<T>,
|
sqlQuery: (sql: string) => Promise<T>,
|
||||||
extractMigrationNames: (result: T) => Set<string>,
|
extractMigrationNames: (result: T) => Set<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log("Will run migrations");
|
|
||||||
|
|
||||||
// Create migrations table if it doesn't exist
|
// Create migrations table if it doesn't exist
|
||||||
const result0 = await sqlExec(`
|
const result0 = await sqlExec(`
|
||||||
@@ -42,29 +40,21 @@ export class MigrationService {
|
|||||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log("Created migrations table", JSON.stringify(result0));
|
|
||||||
|
|
||||||
// Get list of executed migrations
|
// Get list of executed migrations
|
||||||
const result1: T = await sqlQuery("SELECT name FROM migrations;");
|
const result1: T = await sqlQuery("SELECT name FROM migrations;");
|
||||||
const executedMigrations = extractMigrationNames(result1);
|
const executedMigrations = extractMigrationNames(result1);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
|
||||||
"Executed migration select",
|
|
||||||
JSON.stringify(executedMigrations),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run pending migrations in order
|
// Run pending migrations in order
|
||||||
for (const migration of this.migrations) {
|
for (const migration of this.migrations) {
|
||||||
if (!executedMigrations.has(migration.name)) {
|
if (!executedMigrations.has(migration.name)) {
|
||||||
const result2 = await sqlExec(migration.sql);
|
await sqlExec(migration.sql);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log("Executed migration", JSON.stringify(result2));
|
await sqlExec(
|
||||||
const result3 = await sqlExec(
|
|
||||||
`INSERT INTO migrations (name) VALUES ('${migration.name}')`,
|
`INSERT INTO migrations (name) VALUES ('${migration.name}')`,
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log("Updated migrations table", JSON.stringify(result3));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,8 +128,6 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
let result: unknown;
|
let result: unknown;
|
||||||
switch (operation.type) {
|
switch (operation.type) {
|
||||||
case "run": {
|
case "run": {
|
||||||
console.log("[CapacitorPlatformService] running sql:", operation.sql);
|
|
||||||
console.log("[CapacitorPlatformService] params:", operation.params);
|
|
||||||
const runResult = await this.db.run(
|
const runResult = await this.db.run(
|
||||||
operation.sql,
|
operation.sql,
|
||||||
operation.params,
|
operation.params,
|
||||||
@@ -141,8 +139,6 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "query": {
|
case "query": {
|
||||||
console.log("[CapacitorPlatformService] querying sql:", operation.sql);
|
|
||||||
console.log("[CapacitorPlatformService] params:", operation.params);
|
|
||||||
const queryResult = await this.db.query(
|
const queryResult = await this.db.query(
|
||||||
operation.sql,
|
operation.sql,
|
||||||
operation.params,
|
operation.params,
|
||||||
@@ -158,15 +154,9 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
}
|
}
|
||||||
operation.resolve(result);
|
operation.resolve(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// make sure you don't try to log to the DB... infinite loop!
|
logger.error(
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(
|
|
||||||
"[CapacitorPlatformService] Error while processing SQL queue:",
|
"[CapacitorPlatformService] Error while processing SQL queue:",
|
||||||
error,
|
error,
|
||||||
" ... for sql:",
|
|
||||||
operation.sql,
|
|
||||||
" ... with params:",
|
|
||||||
operation.params,
|
|
||||||
);
|
);
|
||||||
operation.reject(error);
|
operation.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1119,7 +1119,6 @@ export default class AccountViewView extends Vue {
|
|||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
console.log("[AccountViewView] mounted");
|
|
||||||
// Initialize component state with values from the database or defaults
|
// Initialize component state with values from the database or defaults
|
||||||
await this.initializeState();
|
await this.initializeState();
|
||||||
await this.processIdentity();
|
await this.processIdentity();
|
||||||
@@ -1172,7 +1171,6 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("[AccountViewView] error: ", JSON.stringify(error, null, 2));
|
|
||||||
// this can happen when running automated tests in dev mode because notifications don't work
|
// this can happen when running automated tests in dev mode because notifications don't work
|
||||||
logger.error(
|
logger.error(
|
||||||
"Telling user to clear cache at page create because:",
|
"Telling user to clear cache at page create because:",
|
||||||
@@ -1206,7 +1204,6 @@ export default class AccountViewView extends Vue {
|
|||||||
this.turnOffNotifyingFlags();
|
this.turnOffNotifyingFlags();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log("Got to the end of 'mounted' call in AccountViewView.");
|
|
||||||
/**
|
/**
|
||||||
* Beware! I've seen where we never get to this point because "ready" never resolves.
|
* Beware! I've seen where we never get to this point because "ready" never resolves.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -521,7 +521,9 @@ export default class HomeView extends Vue {
|
|||||||
logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
|
logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
||||||
throw new Error("Failed to load existing identities. Please try restarting the app.");
|
throw new Error(
|
||||||
|
"Failed to load existing identities. Please try restarting the app.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new DID if needed
|
// Create new DID if needed
|
||||||
@@ -534,7 +536,10 @@ export default class HomeView extends Vue {
|
|||||||
logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`);
|
logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.isCreatingIdentifier = false;
|
this.isCreatingIdentifier = false;
|
||||||
logConsoleAndDb(`[HomeView] Failed to create new identity: ${error}`, true);
|
logConsoleAndDb(
|
||||||
|
`[HomeView] Failed to create new identity: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
throw new Error("Failed to create new identity. Please try again.");
|
throw new Error("Failed to create new identity. Please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,34 +551,53 @@ export default class HomeView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
}
|
}
|
||||||
logConsoleAndDb(`[HomeView] Retrieved settings for ${settings.activeDid || 'no active DID'}`);
|
logConsoleAndDb(
|
||||||
|
`[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[HomeView] Failed to retrieve settings: ${error}`, true);
|
logConsoleAndDb(
|
||||||
throw new Error("Failed to load user settings. Some features may be limited.");
|
`[HomeView] Failed to retrieve settings: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"Failed to load user settings. Some features may be limited.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update component state
|
// Update component state
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
|
|
||||||
// Load contacts with graceful fallback
|
// Load contacts with graceful fallback
|
||||||
try {
|
try {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
|
const dbContacts = await platformService.dbQuery(
|
||||||
this.allContacts = databaseUtil.mapQueryResultToValues(dbContacts) as Contact[];
|
"SELECT * FROM contacts",
|
||||||
|
);
|
||||||
|
this.allContacts = databaseUtil.mapQueryResultToValues(
|
||||||
|
dbContacts,
|
||||||
|
) as Contact[];
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
}
|
}
|
||||||
logConsoleAndDb(`[HomeView] Retrieved ${this.allContacts.length} contacts`);
|
logConsoleAndDb(
|
||||||
|
`[HomeView] Retrieved ${this.allContacts.length} contacts`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[HomeView] Failed to retrieve contacts: ${error}`, true);
|
logConsoleAndDb(
|
||||||
|
`[HomeView] Failed to retrieve contacts: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
this.allContacts = []; // Ensure we have a valid empty array
|
this.allContacts = []; // Ensure we have a valid empty array
|
||||||
this.$notify({
|
this.$notify(
|
||||||
group: "alert",
|
{
|
||||||
type: "warning",
|
group: "alert",
|
||||||
title: "Contact Loading Issue",
|
type: "warning",
|
||||||
text: "Some contact information may be unavailable.",
|
title: "Contact Loading Issue",
|
||||||
}, 5000);
|
text: "Some contact information may be unavailable.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update remaining settings
|
// Update remaining settings
|
||||||
@@ -583,14 +607,17 @@ export default class HomeView extends Vue {
|
|||||||
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
|
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
|
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
|
||||||
this.lastAckedOfferToUserProjectsJwtId = settings.lastAckedOfferToUserProjectsJwtId;
|
this.lastAckedOfferToUserProjectsJwtId =
|
||||||
|
settings.lastAckedOfferToUserProjectsJwtId;
|
||||||
this.searchBoxes = settings.searchBoxes || [];
|
this.searchBoxes = settings.searchBoxes || [];
|
||||||
this.showShortcutBvc = !!settings.showShortcutBvc;
|
this.showShortcutBvc = !!settings.showShortcutBvc;
|
||||||
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
|
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
|
||||||
|
|
||||||
// Check onboarding status
|
// Check onboarding status
|
||||||
if (!settings.finishedOnboarding) {
|
if (!settings.finishedOnboarding) {
|
||||||
(this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home);
|
(this.$refs.onboardingDialog as OnboardingDialog).open(
|
||||||
|
OnboardPage.Home,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check registration status if needed
|
// Check registration status if needed
|
||||||
@@ -613,10 +640,15 @@ export default class HomeView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.isRegistered = true;
|
this.isRegistered = true;
|
||||||
logConsoleAndDb(`[HomeView] User ${this.activeDid} is now registered`);
|
logConsoleAndDb(
|
||||||
|
`[HomeView] User ${this.activeDid} is now registered`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[HomeView] Registration check failed: ${error}`, true);
|
logConsoleAndDb(
|
||||||
|
`[HomeView] Registration check failed: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
// Continue as unregistered - this is expected for new users
|
// Continue as unregistered - this is expected for new users
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -624,8 +656,11 @@ export default class HomeView extends Vue {
|
|||||||
// Initialize feed and offers
|
// Initialize feed and offers
|
||||||
try {
|
try {
|
||||||
// Start feed update in background
|
// Start feed update in background
|
||||||
this.updateAllFeed().catch(error => {
|
this.updateAllFeed().catch((error) => {
|
||||||
logConsoleAndDb(`[HomeView] Background feed update failed: ${error}`, true);
|
logConsoleAndDb(
|
||||||
|
`[HomeView] Background feed update failed: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load new offers if we have an active DID
|
// Load new offers if we have an active DID
|
||||||
@@ -649,23 +684,28 @@ export default class HomeView extends Vue {
|
|||||||
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
||||||
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
||||||
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
||||||
|
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
|
`[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
|
||||||
`${this.numNewOffersToUserProjects} project offers`
|
`${this.numNewOffersToUserProjects} project offers`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[HomeView] Failed to initialize feed/offers: ${error}`, true);
|
logConsoleAndDb(
|
||||||
|
`[HomeView] Failed to initialize feed/offers: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
// Don't throw - we can continue with empty feed
|
// Don't throw - we can continue with empty feed
|
||||||
this.$notify({
|
this.$notify(
|
||||||
group: "alert",
|
{
|
||||||
type: "warning",
|
group: "alert",
|
||||||
title: "Feed Loading Issue",
|
type: "warning",
|
||||||
text: "Some feed data may be unavailable. Pull to refresh.",
|
title: "Feed Loading Issue",
|
||||||
}, 5000);
|
text: "Some feed data may be unavailable. Pull to refresh.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error);
|
this.handleError(error);
|
||||||
throw error; // Re-throw to be caught by mounted()
|
throw error; // Re-throw to be caught by mounted()
|
||||||
@@ -837,10 +877,10 @@ export default class HomeView extends Vue {
|
|||||||
private handleError(err: unknown) {
|
private handleError(err: unknown) {
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
const userMessage = (err as { userMessage?: string })?.userMessage;
|
const userMessage = (err as { userMessage?: string })?.userMessage;
|
||||||
|
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ''}`,
|
`[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ""}`,
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -848,7 +888,9 @@ export default class HomeView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: userMessage || "There was an error loading your data. Please try refreshing the page.",
|
text:
|
||||||
|
userMessage ||
|
||||||
|
"There was an error loading your data. Please try refreshing the page.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -139,10 +139,8 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
this.apiServerInput = settings.apiServer || "";
|
this.apiServerInput = settings.apiServer || "";
|
||||||
|
|
||||||
const accounts = await retrieveAllAccountsMetadata();
|
const accounts = await retrieveAllAccountsMetadata();
|
||||||
console.log("[IdentitySwitcherView] accounts: ", JSON.stringify(accounts, null, 2));
|
|
||||||
for (let n = 0; n < accounts.length; n++) {
|
for (let n = 0; n < accounts.length; n++) {
|
||||||
const acct = accounts[n];
|
const acct = accounts[n];
|
||||||
console.log("[IdentitySwitcherView] acct: ", JSON.stringify(acct, null, 2));
|
|
||||||
this.otherIdentities.push({
|
this.otherIdentities.push({
|
||||||
id: (acct.id ?? 0).toString(),
|
id: (acct.id ?? 0).toString(),
|
||||||
did: acct.did,
|
did: acct.did,
|
||||||
@@ -152,7 +150,6 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[IdentitySwitcherView] error: ", JSON.stringify(err, null, 2));
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -164,7 +161,6 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
);
|
);
|
||||||
logger.error("Telling user to clear cache at page create because:", err);
|
logger.error("Telling user to clear cache at page create because:", err);
|
||||||
}
|
}
|
||||||
console.log("[IdentitySwitcherView] end");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchAccount(did?: string) {
|
async switchAccount(did?: string) {
|
||||||
|
|||||||
@@ -389,7 +389,12 @@
|
|||||||
{{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }}
|
{{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ libsUtil.formattedAmount(givesTotalsByUnit[0].amount, givesTotalsByUnit[0].unit) }}
|
{{
|
||||||
|
libsUtil.formattedAmount(
|
||||||
|
givesTotalsByUnit[0].amount,
|
||||||
|
givesTotalsByUnit[0].unit,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="givesTotalsByUnit.length > 1">...</span>
|
<span v-if="givesTotalsByUnit.length > 1">...</span>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
Reference in New Issue
Block a user