/** * @fileoverview Base Database Service for Platform Services * @author Matthew Raymer * * This abstract base class provides common database operations that are * identical across all platform implementations. It eliminates code * duplication and ensures consistency in database operations. * * Key Features: * - Common database utility methods * - Consistent settings management * - Active identity management * - Abstract methods for platform-specific database operations * * Architecture: * - Abstract base class with common implementations * - Platform services extend this class * - Platform-specific database operations remain abstract * * @since 1.1.1-beta */ import { logger } from "../../utils/logger"; import { QueryExecResult } from "@/interfaces/database"; /** * Abstract base class for platform-specific database services. * * This class provides common database operations that are identical * across all platform implementations (Web, Capacitor, Electron). * Platform-specific services extend this class and implement the * abstract database operation methods. * * Common Operations: * - Settings management (update, retrieve, insert) * - Active identity management * - Database utility methods * * @abstract * @example * ```typescript * export class WebPlatformService extends BaseDatabaseService { * async dbQuery(sql: string, params?: unknown[]): Promise { * // Web-specific implementation * } * } * ``` */ export abstract class BaseDatabaseService { /** * Generate an INSERT statement for a model object. * * Creates a parameterized INSERT statement with placeholders for * all properties in the model object. This ensures safe SQL * execution and prevents SQL injection. * * @param model - Object containing the data to insert * @param tableName - Name of the target table * @returns Object containing the SQL statement and parameters * * @example * ```typescript * const { sql, params } = this.generateInsertStatement( * { name: 'John', age: 30 }, * 'users' * ); * // sql: "INSERT INTO users (name, age) VALUES (?, ?)" * // params: ['John', 30] * ``` */ generateInsertStatement( model: Record, tableName: string, ): { sql: string; params: unknown[] } { const keys = Object.keys(model); const placeholders = keys.map(() => "?").join(", "); const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`; const params = keys.map((key) => model[key]); return { sql, params }; } /** * Update default settings for the currently active account. * * Retrieves the active DID from the active_identity table and updates * the corresponding settings record. This ensures settings are always * updated for the correct account. * * @param settings - Object containing the settings to update * @returns Promise that resolves when settings are updated * * @throws {Error} If no active DID is found or database operation fails * * @example * ```typescript * await this.updateDefaultSettings({ * theme: 'dark', * notifications: true * }); * ``` */ async updateDefaultSettings( settings: Record, ): Promise { // Get current active DID and update that identity's settings const activeIdentity = await this.getActiveIdentity(); const activeDid = activeIdentity.activeDid; if (!activeDid) { logger.warn( "[BaseDatabaseService] No active DID found, cannot update default settings", ); return; } const keys = Object.keys(settings); const setClause = keys.map((key) => `${key} = ?`).join(", "); const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`; const params = [...keys.map((key) => settings[key]), activeDid]; await this.dbExec(sql, params); } /** * Update the active DID in the active_identity table. * * Sets the active DID and updates the lastUpdated timestamp. * This is used when switching between different accounts/identities. * * @param did - The DID to set as active * @returns Promise that resolves when the update is complete * * @example * ```typescript * await this.updateActiveDid('did:example:123'); * ``` */ async updateActiveDid(did: string): Promise { await this.dbExec( "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", [did], ); } /** * Get the currently active DID from the active_identity table. * * Retrieves the active DID that represents the currently selected * account/identity. This is used throughout the application to * ensure operations are performed on the correct account. * * @returns Promise resolving to object containing the active DID * * @example * ```typescript * const { activeDid } = await this.getActiveIdentity(); * console.log('Current active DID:', activeDid); * ``` */ async getActiveIdentity(): Promise<{ activeDid: string }> { const result = (await this.dbQuery( "SELECT activeDid FROM active_identity WHERE id = 1", )) as QueryExecResult; return { activeDid: (result?.values?.[0]?.[0] as string) || "", }; } /** * Insert a new DID into the settings table with default values. * * Creates a new settings record for a DID with default configuration * values. Uses INSERT OR REPLACE to handle cases where settings * already exist for the DID. * * @param did - The DID to create settings for * @returns Promise that resolves when settings are created * * @example * ```typescript * await this.insertNewDidIntoSettings('did:example:123'); * ``` */ async insertNewDidIntoSettings(did: string): Promise { // Import constants dynamically to avoid circular dependencies const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } = await import("@/constants/app"); // Use INSERT OR REPLACE to handle case where settings already exist for this DID // This prevents duplicate accountDid entries and ensures data integrity await this.dbExec( "INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)", [did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER], ); } /** * Update settings for a specific DID. * * Updates settings for a particular DID rather than the active one. * This is useful for bulk operations or when managing multiple accounts. * * @param did - The DID to update settings for * @param settings - Object containing the settings to update * @returns Promise that resolves when settings are updated * * @example * ```typescript * await this.updateDidSpecificSettings('did:example:123', { * theme: 'light', * notifications: false * }); * ``` */ async updateDidSpecificSettings( did: string, settings: Record, ): Promise { const keys = Object.keys(settings); const setClause = keys.map((key) => `${key} = ?`).join(", "); const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`; const params = [...keys.map((key) => settings[key]), did]; await this.dbExec(sql, params); } /** * Retrieve settings for the currently active account. * * Gets the active DID and retrieves all settings for that account. * Excludes the 'id' column from the returned settings object. * * @returns Promise resolving to settings object or null if no active DID * * @example * ```typescript * const settings = await this.retrieveSettingsForActiveAccount(); * if (settings) { * console.log('Theme:', settings.theme); * console.log('Notifications:', settings.notifications); * } * ``` */ async retrieveSettingsForActiveAccount(): Promise | null> { // Get current active DID from active_identity table const activeIdentity = await this.getActiveIdentity(); const activeDid = activeIdentity.activeDid; if (!activeDid) { return null; } const result = (await this.dbQuery( "SELECT * FROM settings WHERE accountDid = ?", [activeDid], )) as QueryExecResult; if (result?.values?.[0]) { // Convert the row to an object const row = result.values[0]; const columns = result.columns || []; const settings: Record = {}; columns.forEach((column: string, index: number) => { if (column !== "id") { // Exclude the id column settings[column] = row[index]; } }); return settings; } return null; } // Abstract methods that must be implemented by platform-specific services /** * Execute a database query (SELECT operations). * * @abstract * @param sql - SQL query string * @param params - Optional parameters for prepared statements * @returns Promise resolving to query results */ abstract dbQuery(sql: string, params?: unknown[]): Promise; /** * Execute a database statement (INSERT, UPDATE, DELETE operations). * * @abstract * @param sql - SQL statement string * @param params - Optional parameters for prepared statements * @returns Promise resolving to execution results */ abstract dbExec(sql: string, params?: unknown[]): Promise; }