You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

333 lines
11 KiB

/**
* 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<boolean> {
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<boolean> {
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<Settings> {
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<Settings> Combined settings with account-specific overrides
* @throws Will log specific errors for debugging but returns default settings on failure
*/
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
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<void> {
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<void> {
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<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 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<string, unknown>,
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<Record<string, unknown>> {
if (!record) {
return [];
}
return mapColumnsToValues(record.columns, record.values) as Array<
Record<string, unknown>
>;
}
/**
* 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<Record<string, unknown>> {
return values.map((row) => {
const obj: Record<string, unknown> = {};
columns.forEach((column, index) => {
obj[column] = row[index];
});
return obj;
});
}