diff --git a/WORKER_ONLY_DATABASE_IMPLEMENTATION.md b/WORKER_ONLY_DATABASE_IMPLEMENTATION.md index ebe1da69..7ec0d22f 100644 --- a/WORKER_ONLY_DATABASE_IMPLEMENTATION.md +++ b/WORKER_ONLY_DATABASE_IMPLEMENTATION.md @@ -7,16 +7,19 @@ This implementation fixes the double migration issue in the TimeSafari web platf ## Problem Solved **Before:** Web platform had dual database contexts: + - Worker thread: `registerSQLWorker.js` β†’ `AbsurdSqlDatabaseService.initialize()` β†’ migrations run - Main thread: `WebPlatformService.dbQuery()` β†’ `databaseService.query()` β†’ migrations run **AGAIN** **After:** Single database context: + - Worker thread: Handles ALL database operations and initializes once - Main thread: Sends messages to worker, no direct database access ## Architecture Changes ### 1. Message-Based Communication + ```typescript // Main Thread (WebPlatformService) await this.sendWorkerMessage({ @@ -36,6 +39,7 @@ onmessage = async (event) => { ``` ### 2. Type-Safe Worker Messages + ```typescript // src/interfaces/worker-messages.ts export interface QueryRequest extends BaseWorkerMessage { @@ -44,7 +48,7 @@ export interface QueryRequest extends BaseWorkerMessage { params?: unknown[]; } -export type WorkerRequest = +export type WorkerRequest = | QueryRequest | ExecRequest | GetOneRowRequest @@ -54,14 +58,16 @@ export type WorkerRequest = ### 3. Circular Dependency Resolution -**πŸ”₯ Critical Fix: Stack Overflow Prevention** +#### πŸ”₯ Critical Fix: Stack Overflow Prevention **Problem**: Circular module dependency caused infinite recursion: + - `WebPlatformService` constructor β†’ creates Worker - Worker loads `registerSQLWorker.js` β†’ imports `databaseService` - Module resolution creates circular dependency β†’ Stack Overflow **Solution**: Lazy Loading in Worker + ```javascript // Before (caused stack overflow) import databaseService from "./services/AbsurdSqlDatabaseService"; @@ -80,6 +86,7 @@ async function getDatabaseService() { ``` **Key Changes for Stack Overflow Fix:** + - βœ… Removed top-level import of database service - βœ… Added lazy loading with dynamic import - βœ… Updated all handlers to use `await getDatabaseService()` @@ -89,18 +96,21 @@ async function getDatabaseService() { ## Implementation Details ### 1. WebPlatformService Changes + - Removed direct database imports - Added worker message handling - Implemented timeout and error handling - All database methods now proxy to worker ### 2. Worker Thread Changes + - Added message-based operation handling - Implemented lazy loading for database service - Added proper error handling and response formatting - Fixed circular dependency with dynamic imports ### 3. Main Thread Changes + - Removed duplicate worker creation in `main.web.ts` - WebPlatformService now manages single worker instance - Added Safari compatibility with `initBackend()` @@ -131,25 +141,30 @@ async function getDatabaseService() { ## Benefits ### βœ… Fixes Double Migration Issue + - Database migrations now run only once in worker thread - No duplicate initialization between main thread and worker ### βœ… Prevents Stack Overflow + - Circular dependency resolved with lazy loading - Worker loads immediately without triggering database import - Database service loads on-demand when first operation occurs ### βœ… Improved Performance + - Single database connection - No redundant operations - Better resource utilization ### βœ… Better Error Handling + - Centralized error handling in worker - Type-safe message communication - Proper timeout handling ### βœ… Consistent Architecture + - Matches Capacitor platform pattern - Single-threaded database access - Clear separation of concerns @@ -159,12 +174,14 @@ async function getDatabaseService() { After implementation, you should see: 1. **Worker Loading**: - ``` + + ```text [SQLWorker] Worker loaded, ready to receive messages ``` 2. **Database Initialization** (only on first operation): - ``` + + ```text [SQLWorker] Starting database initialization... [SQLWorker] Database initialization completed successfully ``` @@ -199,46 +216,51 @@ If upgrading from the dual-context implementation: ## Migration Execution Flow ### Before (Problematic) -``` + +```chart β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ ───┐ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Main Thread β”‚ β”‚ Worker Thread β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ WebPlatformServiceβ”‚ β”‚registerSQLWorkerβ”‚ β”‚ ↓ β”‚ β”‚ ↓ β”‚ β”‚ databaseService β”‚ β”‚ databaseService β”‚ -β”‚ (Instance A) β”‚ β”‚ (Instance B) β”‚ +β”‚ (Instance A) β”‚ β”‚ (Instance B) β”‚ β”‚ ↓ β”‚ β”‚ ↓ β”‚ β”‚ [Run Migrations] β”‚ β”‚[Run Migrations] β”‚ ← DUPLICATE! └─────────────── β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### After (Fixed) -``` + +```text β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ ──┐ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Main Thread β”‚ β”‚ Worker Thread β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ WebPlatformService │───→│registerSQLWorkerβ”‚ β”‚ β”‚ β”‚ ↓ β”‚ β”‚ [Send Messages] β”‚ β”‚ databaseService β”‚ -β”‚ β”‚ β”‚(Single Instance)β”‚ +β”‚ β”‚ β”‚(Single Instance)β”‚ β”‚ β”‚ β”‚ ↓ β”‚ β”‚ β”‚ β”‚[Run Migrations] β”‚ ← ONCE ONLY! └─────────────── β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -## Security Considerations +## New Security Considerations ### 1. **Message Validation** + - All worker messages validated for required fields - Unknown message types rejected with errors - Proper error responses prevent information leakage ### 2. **Timeout Protection** + - 30-second timeout prevents hung operations - Automatic cleanup of pending messages - Worker health checks via ping/pong ### 3. **Error Sanitization** + - Error messages logged but not exposed raw to main thread - Stack traces included only in development - Graceful handling of worker failures @@ -246,17 +268,20 @@ If upgrading from the dual-context implementation: ## Testing Considerations ### 1. **Unit Tests Needed** + - Worker message handling - WebPlatformService worker communication - Error handling and timeouts - Migration execution (should run once only) ### 2. **Integration Tests** + - End-to-end database operations - Worker lifecycle management - Cross-browser compatibility (especially Safari) ### 3. **Performance Tests** + - Message passing overhead - Database operation throughput - Memory usage with worker communication @@ -264,12 +289,14 @@ If upgrading from the dual-context implementation: ## Browser Compatibility ### 1. **Modern Browsers** + - Chrome/Edge: Full SharedArrayBuffer support - Firefox: Full SharedArrayBuffer support (with headers) - Safari: Uses IndexedDB fallback via `initBackend()` ### 2. **Required Headers** -``` + +```text Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` @@ -277,11 +304,13 @@ Cross-Origin-Embedder-Policy: require-corp ## Deployment Notes ### 1. **Development** + - Enhanced logging shows worker message flow - Clear separation between worker and main thread logs - Easy debugging via browser DevTools ### 2. **Production** + - Reduced logging overhead - Optimized message passing - Proper error reporting without sensitive data @@ -289,11 +318,13 @@ Cross-Origin-Embedder-Policy: require-corp ## Future Enhancements ### 1. **Potential Optimizations** + - Message batching for bulk operations - Connection pooling simulation - Persistent worker state management ### 2. **Additional Features** + - Database backup/restore via worker - Schema introspection commands - Performance monitoring hooks @@ -301,6 +332,7 @@ Cross-Origin-Embedder-Policy: require-corp ## Rollback Plan If issues arise, rollback involves: + 1. Restore original `WebPlatformService.ts` 2. Restore original `registerSQLWorker.js` 3. Restore original `main.web.ts` @@ -346,4 +378,4 @@ git commit -m "Document worker-only database implementation - Comprehensive documentation of architecture changes - Explain problem solved and benefits achieved - Include security considerations and testing requirements" -``` \ No newline at end of file +``` diff --git a/doc/databaseUtil-migration-plan.md b/doc/databaseUtil-migration-plan.md new file mode 100644 index 00000000..2cdd64b8 --- /dev/null +++ b/doc/databaseUtil-migration-plan.md @@ -0,0 +1,362 @@ +# DatabaseUtil to PlatformServiceMixin Migration Plan + +## Migration Overview + +This plan migrates database utility functions from `src/db/databaseUtil.ts` to `src/utils/PlatformServiceMixin.ts` to consolidate database operations and reduce boilerplate code across the application. + +## Priority Levels + +### πŸ”΄ **PRIORITY 1 (Critical - Migrate First)** + +Functions used in 50+ files that are core to application functionality + +### 🟑 **PRIORITY 2 (High - Migrate Second)** + +Functions used in 10-50 files that are important but not critical + +### 🟒 **PRIORITY 3 (Medium - Migrate Third)** + +Functions used in 5-10 files that provide utility but aren't frequently used + +### πŸ”΅ **PRIORITY 4 (Low - Migrate Last)** + +Functions used in <5 files or specialized functions + +## Detailed Migration Plan + +### πŸ”΄ **PRIORITY 1 - Critical Functions** + +#### 1. `retrieveSettingsForActiveAccount()` + +- **Usage**: 60+ files +- **Current**: `databaseUtil.retrieveSettingsForActiveAccount()` +- **Target**: `this.$settings()` (already exists in PlatformServiceMixin) +- **Migration**: Replace all calls with `this.$settings()` +- **Files to migrate**: All view files, components, and services + +#### 2. `logConsoleAndDb()` and `logToDb()` + +- **Usage**: 40+ files +- **Current**: `databaseUtil.logConsoleAndDb()` / `databaseUtil.logToDb()` +- **Target**: Add `$log()` and `$logError()` methods to PlatformServiceMixin +- **Migration**: Replace with `this.$log()` and `this.$logError()` +- **Files to migrate**: All error handling and logging code + +#### 3. `mapQueryResultToValues()` and `mapColumnsToValues()` + +- **Usage**: 30+ files +- **Current**: `databaseUtil.mapQueryResultToValues()` / `databaseUtil.mapColumnsToValues()` +- **Target**: `this.$mapResults()` (already exists in PlatformServiceMixin) +- **Migration**: Replace with `this.$mapResults()` +- **Files to migrate**: All data processing components + +### 🟑 **PRIORITY 2 - High Priority Functions** + +#### 4. `updateDefaultSettings()` and `updateDidSpecificSettings()` + +- **Usage**: 20+ files +- **Current**: `databaseUtil.updateDefaultSettings()` / `databaseUtil.updateDidSpecificSettings()` +- **Target**: `this.$saveSettings()` and `this.$saveUserSettings()` (already exist) +- **Migration**: Replace with existing mixin methods +- **Files to migrate**: Settings management components + +#### 5. `parseJsonField()` + +- **Usage**: 15+ files +- **Current**: `databaseUtil.parseJsonField()` or direct import +- **Target**: Add `$parseJson()` method to PlatformServiceMixin +- **Migration**: Replace with `this.$parseJson()` +- **Files to migrate**: Data processing components + +#### 6. `generateInsertStatement()` and `generateUpdateStatement()` + +- **Usage**: 10+ files +- **Current**: `databaseUtil.generateInsertStatement()` / `databaseUtil.generateUpdateStatement()` +- **Target**: `this.$insertEntity()` and `this.$updateEntity()` (expand existing methods) +- **Migration**: Replace with high-level entity methods +- **Files to migrate**: Data manipulation components + +### 🟒 **PRIORITY 3 - Medium Priority Functions** + +#### 7. `insertDidSpecificSettings()` + +- **Usage**: 8 files +- **Current**: `databaseUtil.insertDidSpecificSettings()` +- **Target**: `this.$insertUserSettings()` (new method) +- **Migration**: Replace with new mixin method +- **Files to migrate**: Account creation and import components + +#### 8. `debugSettingsData()` + +- **Usage**: 5 files +- **Current**: `databaseUtil.debugSettingsData()` +- **Target**: `this.$debugSettings()` (new method) +- **Migration**: Replace with new mixin method +- **Files to migrate**: Debug and testing components + +### πŸ”΅ **PRIORITY 4 - Low Priority Functions** + +#### 9. `retrieveSettingsForDefaultAccount()` + +- **Usage**: 3 files +- **Current**: `databaseUtil.retrieveSettingsForDefaultAccount()` +- **Target**: `this.$getDefaultSettings()` (new method) +- **Migration**: Replace with new mixin method +- **Files to migrate**: Settings management components + +#### 10. Memory logs and cleanup functions + +- **Usage**: 2 files +- **Current**: `databaseUtil.memoryLogs`, cleanup functions +- **Target**: `this.$memoryLogs` and `this.$cleanupLogs()` (new methods) +- **Migration**: Replace with new mixin methods +- **Files to migrate**: Log management components + +## Implementation Strategy + +### Phase 0: Untangle Logger and DatabaseUtil (Prerequisite) + +**This must be done FIRST to eliminate circular dependencies before any mixin migration.** + +1. **Create self-contained logger.ts**: + - Remove `import { logToDb } from "../db/databaseUtil"` + - Add direct database access via `PlatformServiceFactory.getInstance()` + - Implement `logger.toDb()` and `logger.toConsoleAndDb()` methods + +2. **Remove databaseUtil imports from PlatformServiceMixin**: + - Remove `import { mapColumnsToValues, parseJsonField } from "@/db/databaseUtil"` + - Remove `import * as databaseUtil from "@/db/databaseUtil"` + - Add self-contained implementations of utility methods + +3. **Test logger independence**: + - Verify logger works without databaseUtil + - Ensure no circular dependencies exist + - Test all logging functionality + +### Phase 1: Add Missing Methods to PlatformServiceMixin + +1. **Add logging methods** (now using independent logger): + + ```typescript + $log(message: string, level?: string): Promise + $logError(message: string): Promise + ``` + +2. **Add JSON parsing method** (self-contained): + + ```typescript + $parseJson(value: unknown, defaultValue: T): T + ``` + +3. **Add entity update method**: + + ```typescript + $updateEntity(tableName: string, entity: Record, whereClause: string, whereParams: unknown[]): Promise + ``` + +4. **Add user settings insertion**: + + ```typescript + $insertUserSettings(did: string, settings: Partial): Promise + ``` + +### Phase 2: File-by-File Migration + +#### Migration Order (by priority) + +**Prerequisite**: Phase 0 (Logger/DatabaseUtil untangling) must be completed first. + +1. **Start with most critical files**: + - `src/App.vue` (main application) + - `src/views/AccountViewView.vue` (core account management) + - `src/views/ContactsView.vue` (core contact management) + +2. **Migrate high-usage components**: + - All view files in `src/views/` + - Core components in `src/components/` + +3. **Migrate services and utilities**: + - `src/libs/util.ts` + - `src/services/` files + - `src/utils/logger.ts` + +4. **Migrate remaining components**: + - Specialized components + - Test files + +### Phase 3: Cleanup and Validation + +1. **Remove databaseUtil imports** from migrated files +2. **Update TypeScript interfaces** to reflect new methods +3. **Run comprehensive tests** to ensure functionality +4. **Remove unused databaseUtil functions** after all migrations complete + +## Migration Commands Template + +For each file migration: + +```bash +# 1. Update imports +# Remove: import * as databaseUtil from "../db/databaseUtil"; +# Add: import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; + +# 2. Add mixin to component +# Add: mixins: [PlatformServiceMixin], + +# 3. Replace function calls +# Replace: databaseUtil.retrieveSettingsForActiveAccount() +# With: this.$settings() + +# 4. Test the migration +npm run test + +# 5. Commit the change +git add . +git commit -m "Migrate [filename] from databaseUtil to PlatformServiceMixin" +``` + +## Benefits of Migration + +1. **Reduced Boilerplate**: Eliminate repeated `PlatformServiceFactory.getInstance()` calls +2. **Better Caching**: Leverage existing caching in PlatformServiceMixin +3. **Consistent Error Handling**: Centralized error handling and logging +4. **Type Safety**: Better TypeScript integration with mixin methods +5. **Performance**: Cached platform service access and optimized database operations +6. **Maintainability**: Single source of truth for database operations + +## Risk Mitigation + +1. **Incremental Migration**: Migrate one file at a time to minimize risk +2. **Comprehensive Testing**: Test each migration thoroughly +3. **Rollback Plan**: Keep databaseUtil.ts until all migrations are complete +4. **Documentation**: Update documentation as methods are migrated + +## Smart Logging Integration Strategy + +### Current State Analysis + +#### Current Logging Architecture + +1. **`src/utils/logger.ts`** - Main logger with console + database logging +2. **`src/db/databaseUtil.ts`** - Database-specific logging (`logToDb`, `logConsoleAndDb`) +3. **Circular dependency** - logger.ts imports logToDb from databaseUtil.ts + +#### Current Issues + +- **Circular dependency** between logger and databaseUtil +- **Duplicate functionality** - both systems log to database +- **Inconsistent interfaces** - different method signatures +- **Scattered logging logic** - logging rules spread across multiple files + +### Recommended Solution: Hybrid Approach (Option 3) + +**Core Concept**: Enhanced logger + PlatformServiceMixin convenience methods with **zero circular dependencies**. + +#### Implementation + +```typescript +// 1. Enhanced logger.ts (single source of truth - NO databaseUtil imports) +export const logger = { + // Existing methods... + + // New database-focused methods (self-contained) + toDb: async (message: string, level?: string) => { + // Direct database access without databaseUtil dependency + const platform = PlatformServiceFactory.getInstance(); + await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [ + new Date().toDateString(), + `[${level?.toUpperCase() || 'INFO'}] ${message}` + ]); + }, + + toConsoleAndDb: async (message: string, isError?: boolean) => { + // Console output + if (isError) { + console.error(message); + } else { + console.log(message); + } + // Database output + await logger.toDb(message, isError ? 'error' : 'info'); + }, + + // Component context methods + withContext: (componentName?: string) => ({ + log: (message: string, level?: string) => logger.toDb(`[${componentName}] ${message}`, level), + error: (message: string) => logger.toDb(`[${componentName}] ${message}`, 'error') + }) +}; + +// 2. PlatformServiceMixin convenience methods (NO databaseUtil imports) +methods: { + $log(message: string, level?: string): Promise { + return logger.toDb(message, level); + }, + + $logError(message: string): Promise { + return logger.toDb(message, 'error'); + }, + + $logAndConsole(message: string, isError = false): Promise { + return logger.toConsoleAndDb(message, isError); + }, + + // Self-contained utility methods (no databaseUtil dependency) + $mapResults(results: QueryExecResult | undefined, mapper: (row: unknown[]) => T): T[] { + if (!results) return []; + return results.values.map(mapper); + }, + + $parseJson(value: unknown, defaultValue: T): T { + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return defaultValue; + } + } + return value as T || defaultValue; + } +} +``` + +#### Benefits + +1. **Single source of truth** - logger.ts handles all database logging +2. **No circular dependencies** - logger.ts doesn't import from databaseUtil +3. **Component convenience** - PlatformServiceMixin provides easy access +4. **Backward compatibility** - existing code can be migrated gradually +5. **Context awareness** - logging can include component context +6. **Performance optimized** - caching and batching in logger + +#### Migration Strategy + +1. **Phase 1**: Create self-contained logger.ts with direct database access (no databaseUtil imports) +2. **Phase 2**: Add self-contained convenience methods to PlatformServiceMixin (no databaseUtil imports) +3. **Phase 3**: Migrate existing code to use new methods +4. **Phase 4**: Remove old logging methods from databaseUtil +5. **Phase 5**: Remove databaseUtil imports from PlatformServiceMixin + +#### Key Features + +- **Smart filtering** - prevent logging loops and initialization noise +- **Context tracking** - include component names in logs +- **Performance optimization** - batch database writes +- **Error handling** - graceful fallback when database unavailable +- **Platform awareness** - different behavior for web/mobile/desktop + +### Integration with Migration Plan + +This logging integration will be implemented as part of **Phase 1** of the migration plan, specifically: + +1. **Add logging methods to PlatformServiceMixin** (Priority 1, Item 2) +2. **Migrate logConsoleAndDb and logToDb usage** across all files +3. **Consolidate logging logic** in logger.ts +4. **Remove circular dependencies** between logger and databaseUtil + +--- + +**Author**: Matthew Raymer +**Created**: 2025-07-05 +**Status**: Planning Phase +**Last Updated**: 2025-07-05 diff --git a/src/App.vue b/src/App.vue index 81a5803f..4d7be20c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -332,8 +332,7 @@ import { Vue, Component } from "vue-facing-decorator"; import { NotificationIface } from "./constants/app"; -import * as databaseUtil from "./db/databaseUtil"; -import { logConsoleAndDb } from "./db/databaseUtil"; +import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { logger } from "./utils/logger"; interface Settings { @@ -341,9 +340,12 @@ interface Settings { notifyingReminderTime?: string; } -@Component +@Component({ + mixins: [PlatformServiceMixin], +}) export default class App extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; + $logAndConsole!: (message: string, isError?: boolean) => Promise; stopAsking = false; @@ -396,8 +398,7 @@ export default class App extends Vue { let allGoingOff = false; try { - const settings: Settings = - await databaseUtil.retrieveSettingsForActiveAccount(); + const settings: Settings = await this.$settings(); const notifyingNewActivity = !!settings?.notifyingNewActivityTime; const notifyingReminder = !!settings?.notifyingReminderTime; @@ -418,11 +419,11 @@ export default class App extends Vue { await subscript.unsubscribe(); } } else { - logConsoleAndDb("Subscription object is not available."); + this.$logAndConsole("Subscription object is not available."); } }) .catch((error) => { - logConsoleAndDb( + this.$logAndConsole( "Push provider server communication failed: " + JSON.stringify(error), true, @@ -458,7 +459,7 @@ export default class App extends Vue { .then(async (response) => { if (!response.ok) { const errorBody = await response.text(); - logConsoleAndDb( + this.$logAndConsole( `Push server failed: ${response.status} ${errorBody}`, true, ); @@ -467,7 +468,7 @@ export default class App extends Vue { return response.ok; }) .catch((error) => { - logConsoleAndDb( + this.$logAndConsole( "Push server communication failed: " + JSON.stringify(error), true, ); @@ -495,7 +496,7 @@ export default class App extends Vue { return pushServerSuccess; } catch (error) { - logConsoleAndDb( + this.$logAndConsole( "Error turning off notifications: " + JSON.stringify(error), true, ); diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index 1fa7c0a4..9a058706 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -13,7 +13,7 @@ export type BoundingBox = { */ export type Settings = { // default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID - id?: number; // this is erased for all those entries that are keyed with accountDid + id?: string | number; // this is erased for all those entries that are keyed with accountDid // if supplied, this settings record overrides the master record when the user switches to this account accountDid?: string; // not used in the MASTER_SETTINGS_KEY entry diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 75cfc3b4..cb400209 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -36,7 +36,7 @@ * @author Matthew Raymer * @version 4.1.0 * @since 2025-07-02 - * @updated 2025-01-25 - Added high-level entity operations for code reduction + * @updated 2025-06-25 - Added high-level entity operations for code reduction */ import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; @@ -44,9 +44,7 @@ import type { PlatformService, PlatformCapabilities, } from "@/services/PlatformService"; -import { mapColumnsToValues, parseJsonField } from "@/db/databaseUtil"; import { MASTER_SETTINGS_KEY, type Settings } from "@/db/tables/settings"; -import * as databaseUtil from "@/db/databaseUtil"; import { logger } from "@/utils/logger"; import { Contact } from "@/db/tables/contacts"; import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database"; @@ -151,6 +149,42 @@ export const PlatformServiceMixin = { }, methods: { + // ================================================= + // SELF-CONTAINED UTILITY METHODS (no databaseUtil dependency) + // ================================================= + + /** + * Self-contained implementation of mapColumnsToValues + * Maps database query results to objects with column names as keys + */ + _mapColumnsToValues( + columns: string[], + values: unknown[][], + ): Array> { + return values.map((row) => { + const obj: Record = {}; + columns.forEach((column, index) => { + obj[column] = row[index]; + }); + return obj; + }); + }, + + /** + * Self-contained implementation of parseJsonField + * Safely parses JSON strings with fallback to default value + */ + _parseJsonField(value: unknown, defaultValue: T): T { + if (typeof value === "string") { + try { + return JSON.parse(value); + } catch { + return defaultValue; + } + } + return (value as T) || defaultValue; + }, + // ================================================= // CACHING UTILITY METHODS // ================================================= @@ -302,7 +336,10 @@ export const PlatformServiceMixin = { return fallback; } - const mappedResults = mapColumnsToValues(result.columns, result.values); + const mappedResults = this._mapColumnsToValues( + result.columns, + result.values, + ); if (!mappedResults.length) { return fallback; @@ -312,7 +349,7 @@ export const PlatformServiceMixin = { // Handle JSON field parsing if (settings.searchBoxes) { - settings.searchBoxes = parseJsonField(settings.searchBoxes, []); + settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []); } return settings; @@ -360,7 +397,7 @@ export const PlatformServiceMixin = { } // Map and filter non-null overrides - const mappedResults = mapColumnsToValues( + const mappedResults = this._mapColumnsToValues( accountResult.columns, accountResult.values, ); @@ -383,7 +420,7 @@ export const PlatformServiceMixin = { // Handle JSON field parsing if (mergedSettings.searchBoxes) { - mergedSettings.searchBoxes = parseJsonField( + mergedSettings.searchBoxes = this._parseJsonField( mergedSettings.searchBoxes, [], ); @@ -481,7 +518,10 @@ export const PlatformServiceMixin = { if (!result?.columns || !result?.values) { return []; } - const mappedResults = mapColumnsToValues(result.columns, result.values); + const mappedResults = this._mapColumnsToValues( + result.columns, + result.values, + ); return mappedResults as T[]; }, @@ -590,7 +630,38 @@ export const PlatformServiceMixin = { * @returns Promise Success status */ async $saveSettings(changes: Partial): Promise { - return await databaseUtil.updateDefaultSettings(changes); + try { + // Remove fields that shouldn't be updated + const { accountDid, id, ...safeChanges } = changes; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + void accountDid; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + void id; + + if (Object.keys(safeChanges).length === 0) return true; + + const setParts: string[] = []; + const params: unknown[] = []; + + Object.entries(safeChanges).forEach(([key, value]) => { + if (value !== undefined) { + setParts.push(`${key} = ?`); + params.push(value); + } + }); + + if (setParts.length === 0) return true; + + params.push(MASTER_SETTINGS_KEY); + await this.$dbExec( + `UPDATE settings SET ${setParts.join(", ")} WHERE id = ?`, + params, + ); + return true; + } catch (error) { + logger.error("[PlatformServiceMixin] Error saving settings:", error); + return false; + } }, /** @@ -604,7 +675,40 @@ export const PlatformServiceMixin = { did: string, changes: Partial, ): Promise { - return await databaseUtil.updateDidSpecificSettings(did, changes); + try { + // Remove fields that shouldn't be updated + const { id, ...safeChanges } = changes; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + void id; + safeChanges.accountDid = did; + + if (Object.keys(safeChanges).length === 0) return true; + + const setParts: string[] = []; + const params: unknown[] = []; + + Object.entries(safeChanges).forEach(([key, value]) => { + if (value !== undefined) { + setParts.push(`${key} = ?`); + params.push(value); + } + }); + + if (setParts.length === 0) return true; + + params.push(did); + await this.$dbExec( + `UPDATE settings SET ${setParts.join(", ")} WHERE accountDid = ?`, + params, + ); + return true; + } catch (error) { + logger.error( + "[PlatformServiceMixin] Error saving user settings:", + error, + ); + return false; + } }, /** @@ -828,11 +932,11 @@ export const PlatformServiceMixin = { did?: string, ): Promise { try { - // Use databaseUtil methods which handle the correct schema + // Use self-contained methods which handle the correct schema if (did) { - return await databaseUtil.updateDidSpecificSettings(did, changes); + return await this.$saveUserSettings(did, changes); } else { - return await databaseUtil.updateDefaultSettings(changes); + return await this.$saveSettings(changes); } } catch (error) { logger.error("[PlatformServiceMixin] Error updating settings:", error); @@ -860,6 +964,104 @@ export const PlatformServiceMixin = { params, ); }, + + /** + * Update entity with direct SQL - $updateEntity() + * Eliminates verbose UPDATE patterns for any table + * @param tableName Name of the table to update + * @param entity Object containing fields to update + * @param whereClause WHERE clause for the update (e.g. "id = ?") + * @param whereParams Parameters for the WHERE clause + * @returns Promise Success status + */ + async $updateEntity( + tableName: string, + entity: Record, + whereClause: string, + whereParams: unknown[], + ): Promise { + try { + const setParts: string[] = []; + const params: unknown[] = []; + + Object.entries(entity).forEach(([key, value]) => { + if (value !== undefined) { + setParts.push(`${key} = ?`); + // Convert values to SQLite-compatible types + let convertedValue = value ?? null; + if (convertedValue !== null) { + if (typeof convertedValue === "object") { + // Convert objects and arrays to JSON strings + convertedValue = JSON.stringify(convertedValue); + } else if (typeof convertedValue === "boolean") { + // Convert boolean to integer (0 or 1) + convertedValue = convertedValue ? 1 : 0; + } + } + params.push(convertedValue); + } + }); + + if (setParts.length === 0) return true; + + const sql = `UPDATE ${tableName} SET ${setParts.join(", ")} WHERE ${whereClause}`; + await this.$dbExec(sql, [...params, ...whereParams]); + return true; + } catch (error) { + logger.error( + `[PlatformServiceMixin] Error updating entity in ${tableName}:`, + error, + ); + return false; + } + }, + + /** + * Insert user-specific settings - $insertUserSettings() + * Creates new settings record for a specific DID + * @param did DID identifier for the user + * @param settings Settings to insert (accountDid will be set automatically) + * @returns Promise Success status + */ + async $insertUserSettings( + did: string, + settings: Partial, + ): Promise { + try { + // Ensure accountDid is set and remove id to avoid conflicts + const { id, ...safeSettings } = settings; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + void id; + const insertSettings = { ...safeSettings, accountDid: did }; + + // Convert to SQL-compatible values + const fields = Object.keys(insertSettings); + const values = fields.map((field) => { + const value = insertSettings[field as keyof typeof insertSettings]; + if (value === undefined) return null; + if (typeof value === "object" && value !== null) { + return JSON.stringify(value); + } + if (typeof value === "boolean") { + return value ? 1 : 0; + } + return value; + }); + + const placeholders = fields.map(() => "?").join(", "); + await this.$dbExec( + `INSERT OR REPLACE INTO settings (${fields.join(", ")}) VALUES (${placeholders})`, + values, + ); + return true; + } catch (error) { + logger.error( + "[PlatformServiceMixin] Error inserting user settings:", + error, + ); + return false; + } + }, }, }; @@ -911,6 +1113,16 @@ export interface IPlatformServiceMixin { fields: string[], did?: string, ): Promise; + $updateEntity( + tableName: string, + entity: Record, + whereClause: string, + whereParams: unknown[], + ): Promise; + $insertUserSettings( + did: string, + settings: Partial, + ): Promise; } // TypeScript declaration merging to eliminate (this as any) type assertions @@ -995,5 +1207,15 @@ declare module "@vue/runtime-core" { fields: string[], did?: string, ): Promise; + $updateEntity( + tableName: string, + entity: Record, + whereClause: string, + whereParams: unknown[], + ): Promise; + $insertUserSettings( + did: string, + settings: Partial, + ): Promise; } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 59f3f907..0efe294b 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,4 +1,15 @@ -import { logToDb } from "../db/databaseUtil"; +/** + * Enhanced logger with self-contained database logging + * + * Eliminates circular dependency with databaseUtil by using direct database access. + * Provides comprehensive logging with console and database output. + * + * @author Matthew Raymer + * @version 2.0.0 + * @since 2025-01-25 + */ + +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; export function safeStringify(obj: unknown) { const seen = new WeakSet(); @@ -57,6 +68,32 @@ function shouldSkipDatabaseLogging(message: string): boolean { return initializationMessages.some((pattern) => message.includes(pattern)); } +// Self-contained database logging function +async function logToDatabase( + message: string, + level: string = "info", +): Promise { + // Prevent infinite logging loops + if (isInitializing || shouldSkipDatabaseLogging(message)) { + return; + } + + try { + const platform = PlatformServiceFactory.getInstance(); + const todayKey = new Date().toDateString(); + + await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [ + todayKey, + `[${level.toUpperCase()}] ${message}`, + ]); + } catch (error) { + // Fallback to console if database logging fails + // eslint-disable-next-line no-console + console.error(`[Logger] Database logging failed: ${error}`); + } +} + +// Enhanced logger with self-contained database methods export const logger = { debug: (message: string, ...args: unknown[]) => { // Debug logs are very verbose - only show in development mode for web @@ -66,6 +103,7 @@ export const logger = { } // Don't log debug messages to database to reduce noise }, + log: (message: string, ...args: unknown[]) => { // Regular logs - show in development or for capacitor, but quiet for Electron if ( @@ -76,12 +114,11 @@ export const logger = { console.log(message, ...args); } - // Skip database logging during initialization or for initialization-related messages - if (!shouldSkipDatabaseLogging(message)) { - const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; - logToDb(message + argsString); - } + // Database logging + const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; + logToDatabase(message + argsString, "info"); }, + info: (message: string, ...args: unknown[]) => { if ( process.env.NODE_ENV !== "production" || @@ -92,12 +129,11 @@ export const logger = { console.info(message, ...args); } - // Skip database logging during initialization or for initialization-related messages - if (!shouldSkipDatabaseLogging(message)) { - const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; - logToDb(message + argsString); - } + // Database logging + const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; + logToDatabase(message + argsString, "info"); }, + warn: (message: string, ...args: unknown[]) => { // Always show warnings, but for Electron, suppress routine database warnings if (!isElectron || !message.includes("[CapacitorPlatformService]")) { @@ -105,24 +141,47 @@ export const logger = { console.warn(message, ...args); } - // Skip database logging during initialization or for initialization-related messages - if (!shouldSkipDatabaseLogging(message)) { - const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; - logToDb(message + argsString); - } + // Database logging + const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; + logToDatabase(message + argsString, "warn"); }, + error: (message: string, ...args: unknown[]) => { // Errors will always be logged to console // eslint-disable-next-line no-console console.error(message, ...args); - // Skip database logging during initialization or for initialization-related messages - if (!shouldSkipDatabaseLogging(message)) { - const messageString = safeStringify(message); - const argsString = args.length > 0 ? safeStringify(args) : ""; - logToDb(messageString + argsString); + // Database logging + const messageString = safeStringify(message); + const argsString = args.length > 0 ? safeStringify(args) : ""; + logToDatabase(messageString + argsString, "error"); + }, + + // New database-focused methods (self-contained) + toDb: async (message: string, level?: string): Promise => { + await logToDatabase(message, level || "info"); + }, + + toConsoleAndDb: async (message: string, isError = false): Promise => { + // Console output + if (isError) { + // eslint-disable-next-line no-console + console.error(message); + } else { + // eslint-disable-next-line no-console + console.log(message); } + // Database output + await logToDatabase(message, isError ? "error" : "info"); }, + + // Component context methods + withContext: (componentName?: string) => ({ + log: (message: string, level?: string) => + logToDatabase(`[${componentName}] ${message}`, level), + error: (message: string) => + logToDatabase(`[${componentName}] ${message}`, "error"), + }), }; // Function to manually mark initialization as complete diff --git a/test-playwright/testUtils.ts b/test-playwright/testUtils.ts index f6d5bfac..aa3b320b 100644 --- a/test-playwright/testUtils.ts +++ b/test-playwright/testUtils.ts @@ -200,36 +200,26 @@ export async function generateNewEthrUser(page: Page): Promise { // Generate a new random user and register them. // Note that this makes 000 the active user. Use switchToUser to switch to this DID. export async function generateAndRegisterEthrUser(page: Page): Promise { - console.log('[DEBUG] generateAndRegisterEthrUser: Starting user generation'); const newDid = await generateNewEthrUser(page); - console.log('[DEBUG] generateAndRegisterEthrUser: Generated new DID:', newDid); await importUser(page, '000'); // switch to user 000 - console.log('[DEBUG] generateAndRegisterEthrUser: Switched to user 000'); await page.goto('./contacts'); - console.log('[DEBUG] generateAndRegisterEthrUser: Navigated to contacts page'); const contactName = createContactName(newDid); - console.log('[DEBUG] generateAndRegisterEthrUser: Created contact name:', contactName); const contactInput = `${newDid}, ${contactName}`; - console.log('[DEBUG] generateAndRegisterEthrUser: Filling contact input with:', contactInput); await page.getByPlaceholder('URL or DID, Name, Public Key').fill(contactInput); await page.locator('button > svg.fa-plus').click(); - console.log('[DEBUG] generateAndRegisterEthrUser: Clicked add contact button'); // register them await page.locator('div[role="alert"] button:has-text("Yes")').click(); - console.log('[DEBUG] generateAndRegisterEthrUser: Clicked registration confirmation'); // wait for it to disappear because the next steps may depend on alerts being gone await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden(); - console.log('[DEBUG] generateAndRegisterEthrUser: Registration dialog dismissed'); await expect(page.locator('li', { hasText: contactName })).toBeVisible(); - console.log('[DEBUG] generateAndRegisterEthrUser: Contact is now visible in list:', contactName); return newDid; }