/** * 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 { const platform = PlatformServiceFactory.getInstance(); const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [ MASTER_SETTINGS_KEY, ]); 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; } } export async function retrieveSettingsForActiveAccount(): Promise { const defaultSettings = await retrieveSettingsForDefaultAccount(); if (!defaultSettings.activeDid) { return defaultSettings; } else { const platform = PlatformServiceFactory.getInstance(); const result = await platform.dbQuery( "SELECT * FROM settings WHERE accountDid = ?", [defaultSettings.activeDid], ); const overrideSettings = result ? (mapColumnsToValues(result.columns, result.values)[0] as Settings) : {}; const overrideSettingsFiltered = Object.fromEntries( Object.entries(overrideSettings).filter(([_, v]) => v !== null), ); const settings = { ...defaultSettings, ...overrideSettingsFiltered }; if (settings.searchBoxes) { // @ts-expect-error - the searchBoxes field is a string in the DB settings.searchBoxes = JSON.parse(settings.searchBoxes); } return settings; } } let lastCleanupDate: string | null = null; /** * 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 { // 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, // ); // 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; }); }