/** * This file is the SQL replacement of the index.ts file in the db directory. * That file will eventually be deleted. */ import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings"; import { logger } from "@/utils/logger"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { QueryExecResult } from "@/interfaces/database"; export async function updateDefaultSettings( settingsChanges: Settings, ): Promise { delete settingsChanges.accountDid; // just in case // ensure there is no "id" that would override the key delete settingsChanges.id; try { const platformService = PlatformServiceFactory.getInstance(); const { sql, params } = generateUpdateStatement( settingsChanges, "settings", "id = ?", [MASTER_SETTINGS_KEY], ); const result = await platformService.dbExec(sql, params); return result.changes === 1; } catch (error) { logger.error("Error updating default settings:", error); if (error instanceof Error) { throw error; // Re-throw if it's already an Error with a message } else { throw new Error( `Failed to update settings. We recommend you try again or restart the app.`, ); } } } export async function updateAccountSettings( accountDid: string, settingsChanges: Settings, ): Promise { settingsChanges.accountDid = accountDid; delete settingsChanges.id; // key off account, not ID const platform = PlatformServiceFactory.getInstance(); // First try to update existing record const { sql: updateSql, params: updateParams } = generateUpdateStatement( settingsChanges, "settings", "accountDid = ?", [accountDid], ); const updateResult = await platform.dbExec(updateSql, updateParams); // If no record was updated, insert a new one if (updateResult.changes === 1) { return true; } else { const columns = Object.keys(settingsChanges); const values = Object.values(settingsChanges); const placeholders = values.map(() => "?").join(", "); const insertSql = `INSERT INTO settings (${columns.join(", ")}) VALUES (${placeholders})`; const result = await platform.dbExec(insertSql, values); return result.changes === 1; } } const DEFAULT_SETTINGS: Settings = { id: MASTER_SETTINGS_KEY, activeDid: undefined, apiServer: DEFAULT_ENDORSER_API_SERVER, }; // retrieves default settings export async function retrieveSettingsForDefaultAccount(): Promise { console.log("[databaseUtil] retrieveSettingsForDefaultAccount"); const platform = PlatformServiceFactory.getInstance(); const sql = "SELECT * FROM settings WHERE id = ?"; console.log("[databaseUtil] sql", sql); 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) { return DEFAULT_SETTINGS; } else { const settings = mapColumnsToValues( result.columns, result.values, )[0] as Settings; if (settings.searchBoxes) { // @ts-expect-error - the searchBoxes field is a string in the DB settings.searchBoxes = JSON.parse(settings.searchBoxes); } return settings; } } /** * Retrieves settings for the active account, merging with default settings * * @returns Promise Combined settings with account-specific overrides * @throws Will log specific errors for debugging but returns default settings on failure */ export async function retrieveSettingsForActiveAccount(): Promise { logConsoleAndDb("[databaseUtil] Starting settings retrieval for active account"); try { // Get default settings first const defaultSettings = await retrieveSettingsForDefaultAccount(); logConsoleAndDb(`[databaseUtil] Retrieved default settings (hasActiveDid: ${!!defaultSettings.activeDid})`); // If no active DID, return defaults if (!defaultSettings.activeDid) { logConsoleAndDb("[databaseUtil] No active DID found, returning default settings"); return defaultSettings; } // Get account-specific settings try { const platform = PlatformServiceFactory.getInstance(); const result = await platform.dbQuery( "SELECT * FROM settings WHERE accountDid = ?", [defaultSettings.activeDid], ); if (!result?.values?.length) { logConsoleAndDb(`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`); return defaultSettings; } // Map and filter settings const overrideSettings = mapColumnsToValues(result.columns, result.values)[0] as Settings; const overrideSettingsFiltered = Object.fromEntries( Object.entries(overrideSettings).filter(([_, v]) => v !== null), ); // Merge settings const settings = { ...defaultSettings, ...overrideSettingsFiltered }; // Handle searchBoxes parsing if (settings.searchBoxes) { try { // @ts-expect-error - the searchBoxes field is a string in the DB settings.searchBoxes = JSON.parse(settings.searchBoxes); } catch (error) { logConsoleAndDb( `[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`, true ); // Reset to empty array on parse failure settings.searchBoxes = []; } } logConsoleAndDb( `[databaseUtil] Successfully merged settings for ${defaultSettings.activeDid} ` + `(overrides: ${Object.keys(overrideSettingsFiltered).length})` ); return settings; } catch (error) { logConsoleAndDb( `[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`, true ); // Return defaults on error return defaultSettings; } } catch (error) { logConsoleAndDb(`[databaseUtil] Failed to retrieve default settings: ${error}`, true); // Return minimal default settings on complete failure return { id: MASTER_SETTINGS_KEY, activeDid: undefined, apiServer: DEFAULT_ENDORSER_API_SERVER, }; } } let lastCleanupDate: string | null = null; export let memoryLogs: string[] = []; /** * Logs a message to the database with proper handling of concurrent writes * @param message - The message to log * @author Matthew Raymer */ export async function logToDb(message: string): Promise { const platform = PlatformServiceFactory.getInstance(); const todayKey = new Date().toDateString(); const nowKey = new Date().toISOString(); try { memoryLogs.push(`${new Date().toISOString()} ${message}`); // Try to insert first, if it fails due to UNIQUE constraint, update instead await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [ nowKey, message, ]); // Clean up old logs (keep only last 7 days) - do this less frequently // Only clean up if the date is different from the last cleanup if (!lastCleanupDate || lastCleanupDate !== todayKey) { const sevenDaysAgo = new Date( new Date().getTime() - 7 * 24 * 60 * 60 * 1000, ); memoryLogs = memoryLogs.filter( (log) => log.split(" ")[0] > sevenDaysAgo.toDateString(), ); await platform.dbExec("DELETE FROM logs WHERE date < ?", [ sevenDaysAgo.toDateString(), ]); lastCleanupDate = todayKey; } } catch (error) { // Log to console as fallback // eslint-disable-next-line no-console console.error( "Error logging to database:", error, " ... for original message:", message, ); } } // similar method is in the sw_scripts/additional-scripts.js file export async function logConsoleAndDb( message: string, isError = false, ): Promise { if (isError) { logger.error(`${new Date().toISOString()} ${message}`); } else { logger.log(`${new Date().toISOString()} ${message}`); } await logToDb(message); } /** * 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 */ export function generateInsertStatement( model: Record, 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 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 */ export function generateUpdateStatement( model: Record, tableName: string, whereClause: string, whereParams: unknown[] = [], ): { sql: string; params: unknown[] } { // Filter out undefined/null values and create SET clause 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], }; } export function mapQueryResultToValues( record: QueryExecResult | undefined, ): Array> { if (!record) { return []; } return mapColumnsToValues(record.columns, record.values) as Array< Record >; } /** * Maps an array of column names to an array of value arrays, creating objects where each column name * is mapped to its corresponding value. * @param columns Array of column names to use as object keys * @param values Array of value arrays, where each inner array corresponds to one row of data * @returns Array of objects where each object maps column names to their corresponding values */ export function mapColumnsToValues( columns: string[], values: unknown[][], ): Array> { return values.map((row) => { const obj: Record = {}; columns.forEach((column, index) => { obj[column] = row[index]; }); return obj; }); }