diff --git a/src/services/platforms/BaseDatabaseService.ts b/src/services/platforms/BaseDatabaseService.ts new file mode 100644 index 00000000..9f995c13 --- /dev/null +++ b/src/services/platforms/BaseDatabaseService.ts @@ -0,0 +1,297 @@ +/** + * @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; +} diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index b1907f13..51fb9ce5 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -22,6 +22,7 @@ import { PlatformCapabilities, } from "../PlatformService"; import { logger } from "../../utils/logger"; +import { BaseDatabaseService } from "./BaseDatabaseService"; interface QueuedOperation { type: "run" | "query" | "rawQuery"; @@ -39,7 +40,10 @@ interface QueuedOperation { * - Platform-specific features * - SQLite database operations */ -export class CapacitorPlatformService implements PlatformService { +export class CapacitorPlatformService + extends BaseDatabaseService + implements PlatformService +{ /** Current camera direction */ private currentDirection: CameraDirection = CameraDirection.Rear; @@ -52,6 +56,7 @@ export class CapacitorPlatformService implements PlatformService { private isProcessingQueue: boolean = false; constructor() { + super(); this.sqlite = new SQLiteConnection(CapacitorSQLite); } @@ -1328,110 +1333,8 @@ export class CapacitorPlatformService implements PlatformService { // --- PWA/Web-only methods (no-op for Capacitor) --- public registerServiceWorker(): void {} - // Database utility methods - 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 }; - } - - 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( - "[CapacitorPlatformService] 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); - } - - async updateActiveDid(did: string): Promise { - await this.dbExec( - "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", - [did], - ); - } - - async getActiveIdentity(): Promise<{ activeDid: string }> { - const result = await this.dbQuery( - "SELECT activeDid FROM active_identity WHERE id = 1", - ); - return { - activeDid: (result?.values?.[0]?.[0] as string) || "", - }; - } - - 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], - ); - } - - 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); - } - - 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], - ); - 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, index) => { - if (column !== "id") { - // Exclude the id column - settings[column] = row[index]; - } - }); - - return settings; - } - return null; - } + // Database utility methods - inherited from BaseDatabaseService + // generateInsertStatement, updateDefaultSettings, updateActiveDid, + // getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings, + // retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService } diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index f5edcc28..0bc235b6 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -5,6 +5,7 @@ import { } from "../PlatformService"; import { logger } from "../../utils/logger"; import { QueryExecResult } from "@/interfaces/database"; +import { BaseDatabaseService } from "./BaseDatabaseService"; // Dynamic import of initBackend to prevent worker context errors import type { WorkerRequest, @@ -29,7 +30,10 @@ import type { * Note: File system operations are not available in the web platform * due to browser security restrictions. These methods throw appropriate errors. */ -export class WebPlatformService implements PlatformService { +export class WebPlatformService + extends BaseDatabaseService + implements PlatformService +{ private static instanceCount = 0; // Debug counter private worker: Worker | null = null; private workerReady = false; @@ -46,6 +50,7 @@ export class WebPlatformService implements PlatformService { private readonly messageTimeout = 30000; // 30 seconds constructor() { + super(); WebPlatformService.instanceCount++; // Use debug level logging for development mode to reduce console noise @@ -670,116 +675,8 @@ export class WebPlatformService implements PlatformService { // SharedArrayBuffer initialization is handled by initBackend call in initializeWorker } - // Database utility methods - 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 }; - } - - 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( - "[WebPlatformService] 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); - } - - async updateActiveDid(did: string): Promise { - await this.dbExec( - "INSERT OR REPLACE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, ?, ?)", - [did, new Date().toISOString()], - ); - } - - async getActiveIdentity(): Promise<{ activeDid: string }> { - const result = await this.dbQuery( - "SELECT activeDid FROM active_identity WHERE id = 1", - ); - return { - activeDid: (result?.values?.[0]?.[0] as string) || "", - }; - } - - 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], - ); - } - - 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]; - // Log update operation for debugging - logger.debug( - "[WebPlatformService] updateDidSpecificSettings", - sql, - JSON.stringify(params, null, 2), - ); - await this.dbExec(sql, params); - } - - 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], - ); - 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, index) => { - if (column !== "id") { - // Exclude the id column - settings[column] = row[index]; - } - }); - - return settings; - } - return null; - } + // Database utility methods - inherited from BaseDatabaseService + // generateInsertStatement, updateDefaultSettings, updateActiveDid, + // getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings, + // retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService }