Browse Source

feat(electron): Implement SQLite database initialization with proper logging

- Add comprehensive logging for database operations
- Implement proper database path handling and permissions
- Set up WAL journal mode and PRAGMA configurations
- Create initial database schema with tables and triggers
- Add retry logic for database operations
- Implement proper error handling and state management

Current state:
- Database initialization works in main process
- Connection creation succeeds with proper permissions
- Schema creation and table setup complete
- Logging system fully implemented
- Known issue: Property name mismatch between main process and renderer
  causing read-only mode conflicts (to be fixed in next commit)

Technical details:
- Uses WAL journal mode for better concurrency
- Implements proper file permissions checking
- Sets up foreign key constraints
- Creates tables: users, time_entries, time_goals, time_goal_entries
- Adds automatic timestamp triggers
- Implements proper connection lifecycle management

Security:
- Proper file permissions (755 for directory)
- No hardcoded credentials
- Proper error handling and logging
- Safe file path handling

Author: Matthew Raymer
pull/134/head
Matthew Raymer 1 week ago
parent
commit
786f07e067
  1. 111
      electron/src/preload.ts
  2. 164
      electron/src/rt/sqlite-init.ts
  3. 242
      src/services/platforms/ElectronPlatformService.ts

111
electron/src/preload.ts

@ -1,76 +1,97 @@
/** /**
* Preload script for Electron * Preload script for Electron
* Sets up context bridge and handles security policies * Sets up secure IPC communication between renderer and main process
*
* @author Matthew Raymer * @author Matthew Raymer
*/ */
import { contextBridge, ipcRenderer } from 'electron'; import { contextBridge, ipcRenderer } from 'electron';
// Enable source maps in development // Simple logger for preload script
if (process.env.NODE_ENV === 'development') { const logger = {
require('source-map-support').install(); log: (...args: unknown[]) => console.log('[Preload]', ...args),
} error: (...args: unknown[]) => console.error('[Preload]', ...args),
info: (...args: unknown[]) => console.info('[Preload]', ...args),
warn: (...args: unknown[]) => console.warn('[Preload]', ...args),
debug: (...args: unknown[]) => console.debug('[Preload]', ...args),
};
// Log that preload is running // Types for SQLite connection options
console.log('[Preload] Script starting...'); interface SQLiteConnectionOptions {
database: string;
version?: number;
readOnly?: boolean;
readonly?: boolean; // Handle both cases
encryption?: string;
mode?: string;
useNative?: boolean;
[key: string]: unknown; // Allow other properties
}
// Create a proxy for the CapacitorSQLite plugin // Create a proxy for the CapacitorSQLite plugin
const createSQLiteProxy = () => { const createSQLiteProxy = () => {
const MAX_RETRIES = 5; const MAX_RETRIES = 3;
const RETRY_DELAY = 1000; // 1 second const RETRY_DELAY = 1000; // 1 second
const withRetry = async (operation: string, ...args: any[]) => { const withRetry = async <T>(operation: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T> => {
let lastError; let lastError: Error | null = null;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try { try {
return await ipcRenderer.invoke(`sqlite-${operation}`, ...args); return await operation(...args);
} catch (error) { } catch (error) {
lastError = error; lastError = error instanceof Error ? error : new Error(String(error));
console.warn(`[Preload] SQLite operation ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
if (attempt < MAX_RETRIES) { if (attempt < MAX_RETRIES) {
logger.warn(`SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
} }
} }
} }
throw new Error(`SQLite operation ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`); throw new Error(`SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`);
};
const wrapOperation = (method: string) => {
return async (...args: unknown[]): Promise<unknown> => {
try {
// For createConnection, ensure readOnly is false
if (method === 'create-connection') {
const options = args[0] as SQLiteConnectionOptions;
if (options && typeof options === 'object') {
// Set readOnly to false and ensure mode is rwc
options.readOnly = false;
options.mode = 'rwc';
// Remove any lowercase readonly property if it exists
delete options.readonly;
}
}
return await withRetry(ipcRenderer.invoke, 'sqlite-' + method, ...args);
} catch (error) {
logger.error(`SQLite ${method} failed:`, error);
throw new Error(`Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
}; };
// Create a proxy that matches the CapacitorSQLite interface
return { return {
echo: (value: string) => withRetry('echo', value), echo: wrapOperation('echo'),
createConnection: (options: any) => withRetry('create-connection', options), createConnection: wrapOperation('create-connection'),
closeConnection: (options: any) => withRetry('close-connection', options), closeConnection: wrapOperation('close-connection'),
execute: (options: any) => withRetry('execute', options), execute: wrapOperation('execute'),
query: (options: any) => withRetry('query', options), query: wrapOperation('query'),
isAvailable: () => withRetry('is-available'), run: wrapOperation('run'),
getPlatform: () => Promise.resolve('electron') isAvailable: wrapOperation('is-available'),
getPlatform: () => Promise.resolve('electron'),
// Add other methods as needed
}; };
}; };
// Set up context bridge for secure IPC communication // Expose only the CapacitorSQLite proxy
contextBridge.exposeInMainWorld('electronAPI', {
// SQLite operations
sqlite: createSQLiteProxy(),
// Database status events
onDatabaseStatus: (callback: (status: { status: string; error?: string }) => void) => {
ipcRenderer.on('database-status', (_event, status) => callback(status));
return () => {
ipcRenderer.removeAllListeners('database-status');
};
}
});
// Expose CapacitorSQLite globally for the plugin system
contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy()); contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy());
// Handle uncaught errors // Log startup
window.addEventListener('unhandledrejection', (event) => { logger.log('Script starting...');
console.error('[Preload] Unhandled promise rejection:', event.reason);
});
window.addEventListener('error', (event) => { // Handle window load
console.error('[Preload] Unhandled error:', event.error); window.addEventListener('load', () => {
logger.log('Script complete');
}); });
// Log that preload is complete
console.log('[Preload] Script complete');

164
electron/src/rt/sqlite-init.ts

@ -624,6 +624,12 @@ export async function initializeSQLite(): Promise<void> {
// Set up IPC handlers // Set up IPC handlers
export function setupSQLiteHandlers(): void { export function setupSQLiteHandlers(): void {
// Add IPC message logging
const logIPCMessage = (channel: string, direction: 'in' | 'out', data?: any) => {
const timestamp = new Date().toISOString();
logger.debug(`[${timestamp}] IPC ${direction.toUpperCase()} ${channel}:`, data);
};
// Remove any existing handlers to prevent duplicates // Remove any existing handlers to prevent duplicates
try { try {
ipcMain.removeHandler('sqlite-is-available'); ipcMain.removeHandler('sqlite-is-available');
@ -639,38 +645,49 @@ export function setupSQLiteHandlers(): void {
// Register all handlers before any are called // Register all handlers before any are called
ipcMain.handle('sqlite-is-available', async () => { ipcMain.handle('sqlite-is-available', async () => {
logIPCMessage('sqlite-is-available', 'in');
try { try {
// Check both plugin instance and initialization state const result = sqlitePlugin !== null && sqliteInitialized;
return sqlitePlugin !== null && sqliteInitialized; logIPCMessage('sqlite-is-available', 'out', { result });
return result;
} catch (error) { } catch (error) {
logger.error('Error in sqlite-is-available:', error); logger.error('Error in sqlite-is-available:', error);
logIPCMessage('sqlite-is-available', 'out', { error: error.message });
return false; return false;
} }
}); });
// Add handler to get initialization error // Add handler to get initialization error
ipcMain.handle('sqlite-get-error', async () => { ipcMain.handle('sqlite-get-error', async () => {
return initializationError ? { logIPCMessage('sqlite-get-error', 'in');
const result = initializationError ? {
message: initializationError.message, message: initializationError.message,
stack: initializationError.stack, stack: initializationError.stack,
name: initializationError.name name: initializationError.name
} : null; } : null;
logIPCMessage('sqlite-get-error', 'out', { result });
return result;
}); });
// Update other handlers to handle unavailable state gracefully // Update other handlers to handle unavailable state gracefully
ipcMain.handle('sqlite-echo', async (_event, value) => { ipcMain.handle('sqlite-echo', async (_event, value) => {
logIPCMessage('sqlite-echo', 'in', { value });
try { try {
if (!sqlitePlugin || !sqliteInitialized) { if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available'); throw new Error('SQLite plugin not available');
} }
return await sqlitePlugin.echo({ value }); const result = await sqlitePlugin.echo({ value });
logIPCMessage('sqlite-echo', 'out', { result });
return result;
} catch (error) { } catch (error) {
logger.error('Error in sqlite-echo:', error); logger.error('Error in sqlite-echo:', error);
logIPCMessage('sqlite-echo', 'out', { error: error.message });
throw error; throw error;
} }
}); });
ipcMain.handle('sqlite-create-connection', async (_event, options) => { ipcMain.handle('sqlite-create-connection', async (_event, options) => {
logIPCMessage('sqlite-create-connection', 'in', options);
try { try {
if (!sqlitePlugin || !sqliteInitialized) { if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available'); throw new Error('SQLite plugin not available');
@ -680,6 +697,17 @@ export function setupSQLiteHandlers(): void {
throw new Error('Database path not initialized'); throw new Error('Database path not initialized');
} }
// First check if database exists
const dbExists = await sqlitePlugin.isDBExists({
database: 'timesafari'
});
debugLog('Database existence check:', {
exists: dbExists,
database: 'timesafari',
path: path.join(dbDir, 'timesafariSQLite.db')
});
// Clean up connection options to be consistent // Clean up connection options to be consistent
const connectionOptions = { const connectionOptions = {
database: 'timesafari', // Base name only database: 'timesafari', // Base name only
@ -698,8 +726,31 @@ export function setupSQLiteHandlers(): void {
actualPath: path.join(dbDir, 'timesafariSQLite.db') actualPath: path.join(dbDir, 'timesafariSQLite.db')
}); });
// If database exists, try to close any existing connections first
if (dbExists?.result) {
debugLog('Database exists, checking for open connections');
try {
const isOpen = await sqlitePlugin.isDBOpen({
database: connectionOptions.database
});
if (isOpen?.result) {
debugLog('Database is open, attempting to close');
await sqlitePlugin.close({
database: connectionOptions.database
});
// Wait a moment for connection to fully close
await delay(500);
}
} catch (closeError) {
logger.warn('Error checking/closing existing database:', closeError);
// Continue anyway, as the database might be in a bad state
}
}
// Create connection (returns undefined but registers internally) // Create connection (returns undefined but registers internally)
const result = await sqlitePlugin.createConnection(connectionOptions); debugLog('Creating database connection');
await sqlitePlugin.createConnection(connectionOptions);
// Wait a moment for connection to be registered // Wait a moment for connection to be registered
await delay(500); await delay(500);
@ -715,56 +766,139 @@ export function setupSQLiteHandlers(): void {
actualPath: path.join(dbDir, 'timesafariSQLite.db') actualPath: path.join(dbDir, 'timesafariSQLite.db')
}); });
if (!isRegistered) { if (!isRegistered?.result) {
throw new Error('Database not registered after createConnection'); throw new Error('Database not registered after createConnection');
} }
// Return success object with more details // Open the database with explicit mode
return { debugLog('Opening database with explicit mode');
await sqlitePlugin.open({
database: connectionOptions.database,
version: connectionOptions.version,
readOnly: false,
encryption: connectionOptions.encryption,
useNative: connectionOptions.useNative,
mode: 'rwc' // Force read-write-create mode
});
debugLog('Database opened, verifying mode');
// Verify the connection is not read-only
const journalMode = await sqlitePlugin.query({
database: connectionOptions.database,
statement: 'PRAGMA journal_mode;'
});
debugLog('Journal mode check:', journalMode);
if (!journalMode?.values?.[0]?.journal_mode ||
journalMode.values[0].journal_mode === 'off') {
// Close the database before throwing
await sqlitePlugin.close({
database: connectionOptions.database
});
throw new Error('Database opened in read-only mode despite options');
}
// Double check we can write
debugLog('Verifying write access');
await sqlitePlugin.execute({
database: connectionOptions.database,
statements: 'CREATE TABLE IF NOT EXISTS _write_test (id INTEGER PRIMARY KEY); DROP TABLE IF EXISTS _write_test;',
transaction: false
});
debugLog('Write access verified');
// Log the result before returning
const result = {
success: true, success: true,
database: connectionOptions.database, database: connectionOptions.database,
isRegistered: true, isRegistered: true,
actualPath: path.join(dbDir, 'timesafariSQLite.db'), isOpen: true,
options: connectionOptions readonly: false,
actualPath: path.join(dbDir, 'timesafariSQLite.db')
}; };
logIPCMessage('sqlite-create-connection', 'out', result);
return result;
} catch (error) { } catch (error) {
logger.error('Error in sqlite-create-connection:', error); logger.error('Error in sqlite-create-connection:', error);
logIPCMessage('sqlite-create-connection', 'out', { error: error.message });
// Try to close the database if it was opened
try {
if (sqlitePlugin) {
await sqlitePlugin.close({
database: 'timesafari'
});
}
} catch (closeError) {
logger.warn('Error closing database after connection error:', closeError);
}
throw error; throw error;
} }
}); });
ipcMain.handle('sqlite-execute', async (_event, options) => { ipcMain.handle('sqlite-execute', async (_event, options) => {
logIPCMessage('sqlite-execute', 'in', options);
try { try {
if (!sqlitePlugin || !sqliteInitialized) { if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available'); throw new Error('SQLite plugin not available');
} }
return await sqlitePlugin.execute(options); const result = await sqlitePlugin.execute(options);
logIPCMessage('sqlite-execute', 'out', { result });
return result;
} catch (error) { } catch (error) {
logger.error('Error in sqlite-execute:', error); logger.error('Error in sqlite-execute:', error);
logIPCMessage('sqlite-execute', 'out', { error: error.message });
throw error; throw error;
} }
}); });
ipcMain.handle('sqlite-query', async (_event, options) => { ipcMain.handle('sqlite-query', async (_event, options) => {
logIPCMessage('sqlite-query', 'in', options);
try { try {
if (!sqlitePlugin || !sqliteInitialized) { if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available'); throw new Error('SQLite plugin not available');
} }
return await sqlitePlugin.query(options); const result = await sqlitePlugin.query(options);
logIPCMessage('sqlite-query', 'out', { result });
return result;
} catch (error) { } catch (error) {
logger.error('Error in sqlite-query:', error); logger.error('Error in sqlite-query:', error);
logIPCMessage('sqlite-query', 'out', { error: error.message });
throw error;
}
});
ipcMain.handle('sqlite-run', async (_event, options) => {
logIPCMessage('sqlite-run', 'in', options);
try {
if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available');
}
const result = await sqlitePlugin.run(options);
logIPCMessage('sqlite-run', 'out', { result });
return result;
} catch (error) {
logger.error('Error in sqlite-run:', error);
logIPCMessage('sqlite-run', 'out', { error: error.message });
throw error; throw error;
} }
}); });
ipcMain.handle('sqlite-close-connection', async (_event, options) => { ipcMain.handle('sqlite-close', async (_event, options) => {
logIPCMessage('sqlite-close', 'in', options);
try { try {
if (!sqlitePlugin || !sqliteInitialized) { if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available'); throw new Error('SQLite plugin not available');
} }
return await sqlitePlugin.closeConnection(options); const result = await sqlitePlugin.close(options);
logIPCMessage('sqlite-close', 'out', { result });
return result;
} catch (error) { } catch (error) {
logger.error('Error in sqlite-close-connection:', error); logger.error('Error in sqlite-close:', error);
logIPCMessage('sqlite-close', 'out', { error: error.message });
throw error; throw error;
} }
}); });

242
src/services/platforms/ElectronPlatformService.ts

@ -42,141 +42,119 @@ export class ElectronPlatformService implements PlatformService {
this.sqlite = new SQLiteConnection(window.CapacitorSQLite); this.sqlite = new SQLiteConnection(window.CapacitorSQLite);
} }
private async withRetry<T>(operation: () => Promise<T>): Promise<T> { private async resetConnection(): Promise<void> {
let lastError: Error | undefined; try {
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) { // Try to close any existing connection
try { if (this.db) {
return await operation(); try {
} catch (error) { await this.db.close();
lastError = error instanceof Error ? error : new Error(String(error)); } catch (e) {
if (attempt < this.MAX_RETRIES) { logger.warn('Error closing existing connection:', e);
console.warn(`Database operation failed (attempt ${attempt}/${this.MAX_RETRIES}), retrying...`, error);
await new Promise(resolve => setTimeout(resolve, this.RETRY_DELAY));
} }
this.db = null;
} }
// Reset state
this.initialized = false;
this.initializationPromise = null;
this.dbFatalError = false;
this.dbConnectionErrorLogged = false;
// Wait a moment for cleanup
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
logger.error('Error resetting connection:', error);
throw error;
} }
throw new Error(`Database operation failed after ${this.MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`);
} }
private async initializeDatabase(): Promise<void> { private async initializeDatabase(): Promise<void> {
// If we have a fatal error, try to recover
if (this.dbFatalError) { if (this.dbFatalError) {
throw new Error("Database is in a fatal error state. Please restart the app."); logger.info('Attempting to recover from fatal error state...');
await this.resetConnection();
} }
if (this.initialized) { if (this.initialized) {
return; return;
} }
if (this.initializationPromise) { if (this.initializationPromise) {
return this.initializationPromise; return this.initializationPromise;
} }
this.initializationPromise = (async () => { this.initializationPromise = (async () => {
try { let retryCount = 0;
// Test SQLite availability using the CapacitorSQLite proxy let lastError: Error | null = null;
const isAvailable = await window.CapacitorSQLite.isAvailable();
if (!isAvailable) {
throw new Error("SQLite is not available in the main process");
}
// Log the arguments to createConnection while (retryCount < this.MAX_RETRIES) {
logger.info("Calling createConnection with:", { try {
dbName: this.dbName, // Test SQLite availability
readOnly: false, const isAvailable = await window.CapacitorSQLite.isAvailable();
encryption: "no-encryption", if (!isAvailable) {
version: 1, throw new Error("SQLite is not available in the main process");
useNative: true,
});
// Create/Open database with native implementation
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
true, // Use native implementation
);
logger.info("createConnection result:", this.db);
if (!this.db || typeof this.db.execute !== 'function') {
if (!this.dbConnectionErrorLogged) {
logger.error("Failed to create a valid database connection");
this.dbConnectionErrorLogged = true;
} }
throw new Error("Failed to create a valid database connection");
}
// Do NOT call open() here; Electron connection is ready after createConnection
// await this.db.open();
// Set journal mode to WAL for better performance
await this.db.execute("PRAGMA journal_mode=WAL;");
// Run migrations // Log the connection parameters
await this.runMigrations(); logger.info("Calling createConnection with:", {
dbName: this.dbName,
readOnly: false,
encryption: 'no-encryption',
version: 1,
useNative: true
});
// Create connection
this.db = await this.sqlite.createConnection(
this.dbName, // database name
false, // readOnly
'no-encryption', // encryption
1, // version
true // useNative
);
this.initialized = true; logger.info("createConnection result:", this.db);
logger.log("[Electron] SQLite database initialized successfully");
// Extra logging for debugging DB creation and permissions if (!this.db || typeof this.db.execute !== 'function') {
try { throw new Error("Failed to create a valid database connection");
logger.info("[Debug] process.cwd():", process.cwd());
} catch (e) { logger.warn("[Debug] Could not log process.cwd()", e); }
try {
logger.info("[Debug] __dirname:", __dirname);
} catch (e) { logger.warn("[Debug] Could not log __dirname", e); }
try {
if (typeof window !== 'undefined' && window.electron && window.electron.getPath) {
logger.info("[Debug] electron.getPath('userData'):", window.electron.getPath('userData'));
logger.info("[Debug] electron.getPath('appPath'):", window.electron.getPath('appPath'));
} }
} catch (e) { logger.warn("[Debug] Could not log electron.getPath", e); }
// Try to log directory contents // Verify connection is not read-only
try { const journalMode = await this.db.query('PRAGMA journal_mode;');
const fs = require('fs'); if (journalMode?.values?.[0]?.journal_mode === 'off') {
logger.info("[Debug] Files in process.cwd():", fs.readdirSync(process.cwd())); throw new Error('Database opened in read-only mode despite options');
logger.info("[Debug] Files in __dirname:", fs.readdirSync(__dirname));
} catch (e) { logger.warn("[Debug] Could not list directory contents", e); }
// Try to log file permissions for likely DB file
try {
const fs = require('fs');
const dbFileCandidates = [
`${process.cwd()}/timesafari.db`,
`${__dirname}/timesafari.db`,
`${process.cwd()}/timesafari`,
`${__dirname}/timesafari`,
];
for (const candidate of dbFileCandidates) {
if (fs.existsSync(candidate)) {
logger.info(`[Debug] DB file candidate exists: ${candidate}`);
logger.info(`[Debug] File stats:`, fs.statSync(candidate));
try {
fs.accessSync(candidate, fs.constants.W_OK);
logger.info(`[Debug] File is writable: ${candidate}`);
} catch (err) {
logger.warn(`[Debug] File is NOT writable: ${candidate}`);
}
}
} }
} catch (e) { logger.warn("[Debug] Could not check DB file permissions", e); }
// Log plugin version if available // Run migrations
try { await this.runMigrations();
if (window.CapacitorSQLite && window.CapacitorSQLite.getPlatform) {
window.CapacitorSQLite.getPlatform().then((platform) => { // Success! Clear any error state
logger.info("[Debug] CapacitorSQLite platform:", platform); this.dbFatalError = false;
}); this.dbConnectionErrorLogged = false;
this.initialized = true;
return;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
retryCount++;
if (retryCount < this.MAX_RETRIES) {
logger.warn(`Database initialization attempt ${retryCount}/${this.MAX_RETRIES} failed:`, error);
await new Promise(resolve => setTimeout(resolve, this.RETRY_DELAY));
await this.resetConnection();
} }
} catch (e) { logger.warn("[Debug] Could not log plugin version/platform", e); }
// Comment: To specify the database location in Capacitor SQLite for Electron, you typically only provide the database name. The plugin will create the DB in the app's user data directory. If you need to control the path, check the plugin's Electron docs or source for a 'location' or 'directory' option in createConnection. If not available, the DB will be created in the default location (usually app user data dir).
} catch (error) {
this.dbFatalError = true;
if (!this.dbConnectionErrorLogged) {
logger.error("[Electron] Error initializing SQLite database:", error);
this.dbConnectionErrorLogged = true;
} }
this.initialized = false;
this.initializationPromise = null;
throw error;
} }
// If we get here, all retries failed
this.dbFatalError = true;
if (!this.dbConnectionErrorLogged) {
logger.error("[Electron] Error initializing SQLite database after all retries:", lastError);
this.dbConnectionErrorLogged = true;
}
this.initialized = false;
this.initializationPromise = null;
throw lastError || new Error("Failed to initialize database after all retries");
})(); })();
return this.initializationPromise; return this.initializationPromise;
@ -460,26 +438,7 @@ export class ElectronPlatformService implements PlatformService {
} }
try { try {
await this.withRetry(async () => { await this.initializeDatabase();
const isAvailable = await window.CapacitorSQLite.isAvailable();
if (!isAvailable) {
throw new Error('SQLite is not available in this environment');
}
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
true, // Use native implementation
);
if (!this.db || typeof this.db.execute !== 'function') {
throw new Error("Failed to create a valid database connection");
}
await this.db.execute("PRAGMA journal_mode=WAL;");
await this.runMigrations();
this.initialized = true;
});
} catch (error) { } catch (error) {
console.error('Failed to initialize database:', error); console.error('Failed to initialize database:', error);
throw new Error(`Database initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw new Error(`Database initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
@ -494,14 +453,16 @@ export class ElectronPlatformService implements PlatformService {
throw new Error('Database not initialized. Call initialize() first.'); throw new Error('Database not initialized. Call initialize() first.');
} }
return this.withRetry(async () => { return this.initializeDatabase().then(() => {
try { if (!this.db) {
const result = await this.db?.query(sql, params); throw new Error('Database not initialized after initialization');
return result?.values as T[];
} catch (error) {
console.error('Query failed:', { sql, params, error });
throw new Error(`Query failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
} }
return this.db.query(sql, params).then((result) => {
if (!result?.values) {
return [] as T[];
}
return result.values as T[];
});
}); });
} }
@ -510,13 +471,8 @@ export class ElectronPlatformService implements PlatformService {
throw new Error('Database not initialized. Call initialize() first.'); throw new Error('Database not initialized. Call initialize() first.');
} }
await this.withRetry(async () => { await this.initializeDatabase().then(() => {
try { return this.db?.run(sql, params);
await this.db?.run(sql, params);
} catch (error) {
console.error('Execute failed:', { sql, params, error });
throw new Error(`Execute failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}); });
} }

Loading…
Cancel
Save