Browse Source

Remove debug logging from generateAndRegisterEthrUser test utility

Clean up verbose console.log statements that were cluttering test output.
The function now performs the same operations without debug noise,
making test runs cleaner and more focused on actual test results.
pull/142/head
Matthew Raymer 3 weeks ago
parent
commit
8feb2e6074
  1. 54
      WORKER_ONLY_DATABASE_IMPLEMENTATION.md
  2. 362
      doc/databaseUtil-migration-plan.md
  3. 21
      src/App.vue
  4. 2
      src/db/tables/settings.ts
  5. 248
      src/utils/PlatformServiceMixin.ts
  6. 101
      src/utils/logger.ts
  7. 10
      test-playwright/testUtils.ts

54
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<QueryResult>({
@ -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"
```
```

362
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<void>
$logError(message: string): Promise<void>
```
2. **Add JSON parsing method** (self-contained):
```typescript
$parseJson<T>(value: unknown, defaultValue: T): T
```
3. **Add entity update method**:
```typescript
$updateEntity(tableName: string, entity: Record<string, unknown>, whereClause: string, whereParams: unknown[]): Promise<boolean>
```
4. **Add user settings insertion**:
```typescript
$insertUserSettings(did: string, settings: Partial<Settings>): Promise<boolean>
```
### 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<void> {
return logger.toDb(message, level);
},
$logError(message: string): Promise<void> {
return logger.toDb(message, 'error');
},
$logAndConsole(message: string, isError = false): Promise<void> {
return logger.toConsoleAndDb(message, isError);
},
// Self-contained utility methods (no databaseUtil dependency)
$mapResults<T>(results: QueryExecResult | undefined, mapper: (row: unknown[]) => T): T[] {
if (!results) return [];
return results.values.map(mapper);
},
$parseJson<T>(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

21
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<void>;
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,
);

2
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

248
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<Record<string, unknown>> {
return values.map((row) => {
const obj: Record<string, unknown> = {};
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<T>(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<boolean> Success status
*/
async $saveSettings(changes: Partial<Settings>): Promise<boolean> {
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<Settings>,
): Promise<boolean> {
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<boolean> {
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<boolean> Success status
*/
async $updateEntity(
tableName: string,
entity: Record<string, unknown>,
whereClause: string,
whereParams: unknown[],
): Promise<boolean> {
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<boolean> Success status
*/
async $insertUserSettings(
did: string,
settings: Partial<Settings>,
): Promise<boolean> {
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<unknown[] | undefined>;
$updateEntity(
tableName: string,
entity: Record<string, unknown>,
whereClause: string,
whereParams: unknown[],
): Promise<boolean>;
$insertUserSettings(
did: string,
settings: Partial<Settings>,
): Promise<boolean>;
}
// TypeScript declaration merging to eliminate (this as any) type assertions
@ -995,5 +1207,15 @@ declare module "@vue/runtime-core" {
fields: string[],
did?: string,
): Promise<unknown[] | undefined>;
$updateEntity(
tableName: string,
entity: Record<string, unknown>,
whereClause: string,
whereParams: unknown[],
): Promise<boolean>;
$insertUserSettings(
did: string,
settings: Partial<Settings>,
): Promise<boolean>;
}
}

101
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<void> {
// 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<void> => {
await logToDatabase(message, level || "info");
},
toConsoleAndDb: async (message: string, isError = false): Promise<void> => {
// 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

10
test-playwright/testUtils.ts

@ -200,36 +200,26 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
// 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<string> {
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;
}

Loading…
Cancel
Save