diff --git a/electron/capacitor.config.json b/electron/capacitor.config.json index d7d003d0..1c73a7f2 100644 --- a/electron/capacitor.config.json +++ b/electron/capacitor.config.json @@ -34,8 +34,7 @@ "CapacitorSQLite": { "electronIsEncryption": false, "electronMacLocation": "~/Library/Application Support/TimeSafari", - "electronWindowsLocation": "C:\\ProgramData\\TimeSafari", - "electronLinuxLocation": "~/.local/share/TimeSafari" + "electronWindowsLocation": "C:\\ProgramData\\TimeSafari" } }, "ios": { diff --git a/electron/src/rt/sqlite-init.ts b/electron/src/rt/sqlite-init.ts index 2089daac..24385903 100644 --- a/electron/src/rt/sqlite-init.ts +++ b/electron/src/rt/sqlite-init.ts @@ -3,20 +3,16 @@ * Handles database path setup, plugin initialization, and IPC handlers * * Database Path Handling: - * - Uses XDG Base Directory Specification for data storage + * - Uses plugin's default path (/home/matthew/Databases/TimeSafari) * - Uses modern plugin conventions (Capacitor 6+ / Plugin 6.x+) - * - Supports custom database names without enforced extensions - * - Maintains backward compatibility with legacy paths - * - * XDG Base Directory Specification: - * - Uses $XDG_DATA_HOME (defaults to ~/.local/share) for data files - * - Falls back to legacy path if XDG environment variables are not set + * - Lets plugin handle SQLite suffix and database naming * * @author Matthew Raymer */ import { app, ipcMain } from 'electron'; import { CapacitorSQLite } from '@capacitor-community/sqlite/electron/dist/plugin.js'; +import * as SQLiteModule from '@capacitor-community/sqlite/electron/dist/plugin.js'; import fs from 'fs'; import path from 'path'; import os from 'os'; @@ -30,50 +26,64 @@ const logger = { debug: (...args: unknown[]) => console.debug('[SQLite]', ...args), }; -// Database path resolution utilities following XDG Base Directory Specification +// Add debug logging utility +const debugLog = (stage: string, data?: any) => { + const timestamp = new Date().toISOString(); + if (data) { + logger.debug(`[${timestamp}] ${stage}:`, data); + } else { + logger.debug(`[${timestamp}] ${stage}`); + } +}; + +// Helper to get all methods from an object, including prototype chain +const getAllMethods = (obj: any): string[] => { + if (!obj) return []; + const methods = new Set(); + + // Get own methods + Object.getOwnPropertyNames(obj).forEach(prop => { + if (typeof obj[prop] === 'function') { + methods.add(prop); + } + }); + + // Get prototype methods + let proto = Object.getPrototypeOf(obj); + while (proto && proto !== Object.prototype) { + Object.getOwnPropertyNames(proto).forEach(prop => { + if (typeof proto[prop] === 'function' && !methods.has(prop)) { + methods.add(prop); + } + }); + proto = Object.getPrototypeOf(proto); + } + + return Array.from(methods); +}; + +// Database path resolution utilities const getAppDataPath = async (): Promise => { try { - // Get XDG_DATA_HOME or fallback to default - const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'); - logger.info('XDG_DATA_HOME:', xdgDataHome); - - // Create app data directory following XDG spec - const appDataDir = path.join(xdgDataHome, 'timesafari'); + // Use plugin's actual default path + const appDataDir = path.join(os.homedir(), 'Databases', 'TimeSafari'); logger.info('App data directory:', appDataDir); - // Ensure directory exists with proper permissions (700) + // Ensure directory exists with proper permissions (755) if (!fs.existsSync(appDataDir)) { await fs.promises.mkdir(appDataDir, { recursive: true, - mode: 0o700 // rwx------ for security + mode: 0o755 // rwxr-xr-x to match plugin's expectations }); } else { // Ensure existing directory has correct permissions - await fs.promises.chmod(appDataDir, 0o700); + await fs.promises.chmod(appDataDir, 0o755); } return appDataDir; } catch (error) { logger.error('Error getting app data path:', error); - // Fallback to legacy path in user's home - const fallbackDir = path.join(os.homedir(), '.timesafari'); - logger.warn('Using fallback app data directory:', fallbackDir); - - try { - if (!fs.existsSync(fallbackDir)) { - await fs.promises.mkdir(fallbackDir, { - recursive: true, - mode: 0o700 - }); - } else { - await fs.promises.chmod(fallbackDir, 0o700); - } - } catch (mkdirError) { - logger.error('Failed to create fallback directory:', mkdirError); - throw new Error('Could not create any suitable data directory'); - } - - return fallbackDir; + throw error; } }; @@ -89,40 +99,45 @@ const initializeDatabasePaths = async (): Promise => { dbPathInitializationPromise = (async () => { try { - // Get the app data directory in user's home + // Get the absolute app data directory const absolutePath = await getAppDataPath(); logger.info('Absolute database path:', absolutePath); - // For the plugin, we need to use a path relative to the electron folder - // So we'll use a relative path from the electron folder to the home directory - const electronPath = app.getAppPath(); - const relativeToElectron = path.relative(electronPath, absolutePath); - dbDir = relativeToElectron; + // Use absolute paths for everything + dbDir = absolutePath; + + // Use the exact format the plugin expects + const dbFileName = 'timesafariSQLite.db'; + dbPath = path.join(dbDir, dbFileName); - logger.info('Database directory (relative to electron):', dbDir); + logger.info('Database directory:', dbDir); + logger.info('Database path:', dbPath); - // Ensure directory exists using absolute path + // Ensure directory exists if (!fs.existsSync(absolutePath)) { await fs.promises.mkdir(absolutePath, { recursive: true }); } - // Use modern plugin conventions - no enforced extension - const baseName = 'timesafariSQLite'; - dbPath = path.join(dbDir, baseName); - logger.info('Database path initialized (relative to electron):', dbPath); - - // Verify we can write to the directory using absolute path + // Verify we can write to the directory const testFile = path.join(absolutePath, '.write-test'); try { await fs.promises.writeFile(testFile, 'test'); await fs.promises.unlink(testFile); + logger.info('Directory write test successful'); } catch (error) { throw new Error(`Cannot write to database directory: ${error instanceof Error ? error.message : 'Unknown error'}`); } - // Set environment variable for the plugin using relative path - process.env.CAPACITOR_SQLITE_DB_PATH = dbDir; - logger.info('Set CAPACITOR_SQLITE_DB_PATH:', process.env.CAPACITOR_SQLITE_DB_PATH); + // Verify the path exists and is writable + logger.info('Verifying database directory permissions...'); + const stats = await fs.promises.stat(absolutePath); + logger.info('Directory permissions:', { + mode: stats.mode.toString(8), + uid: stats.uid, + gid: stats.gid, + isDirectory: stats.isDirectory(), + isWritable: !!(stats.mode & 0o200) + }); dbPathInitialized = true; } catch (error) { @@ -144,6 +159,37 @@ let sqlitePlugin: any = null; let sqliteInitialized = false; let sqliteInitializationPromise: Promise | null = null; +// Helper to get the actual plugin instance +const getActualPluginInstance = (plugin: any): any => { + // Try to get the actual instance through various means + const possibleInstances = [ + plugin, // The object itself + plugin.default, // If it's a module + plugin.CapacitorSQLite, // If it's a namespace + Object.getPrototypeOf(plugin), // Its prototype + plugin.constructor?.prototype, // Constructor's prototype + Object.getPrototypeOf(Object.getPrototypeOf(plugin)) // Grandparent prototype + ]; + + // Find the first instance that has createConnection + const instance = possibleInstances.find(inst => + inst && typeof inst === 'object' && typeof inst.createConnection === 'function' + ); + + if (!instance) { + debugLog('No valid plugin instance found in:', { + possibleInstances: possibleInstances.map(inst => ({ + type: typeof inst, + constructor: inst?.constructor?.name, + hasCreateConnection: inst && typeof inst.createConnection === 'function' + })) + }); + return null; + } + + return instance; +}; + export async function initializeSQLite(): Promise { if (sqliteInitializationPromise) { logger.info('SQLite initialization already in progress, waiting...'); @@ -168,12 +214,67 @@ export async function initializeSQLite(): Promise { // Create plugin instance logger.info('Creating SQLite plugin instance...'); - sqlitePlugin = new CapacitorSQLite(); + debugLog('SQLite module:', { + hasDefault: !!SQLiteModule.default, + defaultType: typeof SQLiteModule.default, + defaultKeys: SQLiteModule.default ? Object.keys(SQLiteModule.default) : null, + hasCapacitorSQLite: !!SQLiteModule.CapacitorSQLite, + CapacitorSQLiteType: typeof SQLiteModule.CapacitorSQLite + }); + + // Try both the class and default export + let rawPlugin; + try { + // Try default export first + if (SQLiteModule.default?.CapacitorSQLite) { + debugLog('Using default export CapacitorSQLite'); + rawPlugin = new SQLiteModule.default.CapacitorSQLite(); + } else { + debugLog('Using direct CapacitorSQLite class'); + rawPlugin = new CapacitorSQLite(); + } + } catch (error) { + debugLog('Error creating plugin instance:', error); + throw error; + } - if (!sqlitePlugin) { + if (!rawPlugin) { throw new Error('Failed to create SQLite plugin instance'); } + + // Get the actual plugin instance + sqlitePlugin = getActualPluginInstance(rawPlugin); + if (!sqlitePlugin) { + throw new Error('Failed to get valid SQLite plugin instance'); + } + + // Debug plugin instance + debugLog('Plugin instance details:', { + type: typeof sqlitePlugin, + constructor: sqlitePlugin.constructor?.name, + prototype: Object.getPrototypeOf(sqlitePlugin)?.constructor?.name, + allMethods: getAllMethods(sqlitePlugin), + hasCreateConnection: typeof sqlitePlugin.createConnection === 'function', + createConnectionType: typeof sqlitePlugin.createConnection, + createConnectionProto: Object.getPrototypeOf(sqlitePlugin.createConnection)?.constructor?.name, + // Add more details about the instance + isProxy: sqlitePlugin.constructor?.name === 'Proxy', + descriptors: Object.getOwnPropertyDescriptors(sqlitePlugin), + prototypeChain: (() => { + const chain = []; + let proto = sqlitePlugin; + while (proto && proto !== Object.prototype) { + chain.push({ + name: proto.constructor?.name, + methods: Object.getOwnPropertyNames(proto) + }); + proto = Object.getPrototypeOf(proto); + } + return chain; + })() + }); + // Test the plugin logger.info('Testing SQLite plugin...'); const echoResult = await sqlitePlugin.echo({ value: 'test' }); @@ -182,47 +283,239 @@ export async function initializeSQLite(): Promise { } logger.info('SQLite plugin echo test successful'); - // Initialize database connection using modern plugin conventions + // Initialize database connection using plugin's default format + debugLog('Starting connection creation'); + debugLog('Plugin utilities:', { + sqliteUtil: sqlitePlugin.sqliteUtil ? Object.keys(sqlitePlugin.sqliteUtil) : null, + fileUtil: sqlitePlugin.fileUtil ? Object.keys(sqlitePlugin.fileUtil) : null, + globalUtil: sqlitePlugin.globalUtil ? Object.keys(sqlitePlugin.globalUtil) : null, + prototype: Object.getOwnPropertyNames(Object.getPrototypeOf(sqlitePlugin)) + }); + + // Get the database path using Path.join + const fullDbPath = sqlitePlugin.fileUtil?.Path?.join(dbDir, 'timesafariSQLite.db'); + debugLog('Database path from Path.join:', { + path: fullDbPath, + exists: fullDbPath ? fs.existsSync(fullDbPath) : false, + pathType: typeof sqlitePlugin.fileUtil?.Path?.join + }); + + // Let plugin handle database naming and suffix const connectionOptions = { - database: 'timesafariSQLite', + database: 'timesafari', // Base name only version: 1, readOnly: false, encryption: 'no-encryption', useNative: true, mode: 'rwc', - location: dbDir // Use path relative to electron folder + location: 'default', // Let plugin handle path resolution + path: fullDbPath // Add explicit path }; - logger.info('Creating initial database connection with options:', { + debugLog('Connection options:', { ...connectionOptions, - expectedPath: dbPath // Path relative to electron folder + absoluteLocation: dbDir, + fullDbPath, + expectedBehavior: 'Using prototype methods with explicit path' }); - - const db = await sqlitePlugin.createConnection(connectionOptions); - - if (!db || typeof db !== 'object') { - throw new Error(`Failed to create database connection - invalid response. Path: ${dbPath}`); + + // Verify directory state before connection + try { + const dirStats = await fs.promises.stat(dbDir); + debugLog('Directory state before connection:', { + exists: true, + isDirectory: dirStats.isDirectory(), + mode: dirStats.mode.toString(8), + uid: dirStats.uid, + gid: dirStats.gid, + size: dirStats.size, + atime: dirStats.atime, + mtime: dirStats.mtime, + ctime: dirStats.ctime + }); + + // Check if database file already exists + try { + const dbStats = await fs.promises.stat(fullDbPath); + debugLog('Database file exists:', { + size: dbStats.size, + mode: dbStats.mode.toString(8), + mtime: dbStats.mtime + }); + } catch (error) { + debugLog('Database file does not exist yet (this is expected)'); + } + } catch (error) { + debugLog('Error checking directory state:', error); } - - // Verify connection with a simple query - logger.info('Verifying database connection...'); + + // Create connection using prototype methods + debugLog('Calling createConnection...'); + let db; try { - const result = await db.query({ statement: 'SELECT 1 as test;' }); - if (!result || !result.values || result.values.length === 0) { - throw new Error('Database connection test query returned no results'); + // Get the prototype methods + const proto = Object.getPrototypeOf(sqlitePlugin); + debugLog('Prototype methods:', { + methods: Object.getOwnPropertyNames(proto), + createConnectionType: typeof proto.createConnection, + openType: typeof proto.open, + createConnectionProto: proto.createConnection?.prototype?.constructor?.name + }); + + // Try to create connection using prototype method + if (typeof proto.createConnection === 'function') { + debugLog('Using prototype createConnection'); + + // First try to create the connection + const boundCreateConnection = proto.createConnection.bind(sqlitePlugin); + const result = await boundCreateConnection(connectionOptions); + + debugLog('createConnection raw result:', { + type: typeof result, + isNull: result === null, + isUndefined: result === undefined, + value: result + }); + + // Try to open the database directly + debugLog('Attempting to open database directly...'); + try { + const openResult = await sqlitePlugin.open({ + database: connectionOptions.database, + path: fullDbPath, + version: connectionOptions.version, + readOnly: connectionOptions.readOnly, + encryption: connectionOptions.encryption, + useNative: connectionOptions.useNative, + mode: connectionOptions.mode + }); + + debugLog('open result:', { + type: typeof openResult, + isNull: openResult === null, + isUndefined: openResult === undefined, + value: openResult + }); + + // Try to get the connection after opening + if (sqlitePlugin.getDatabaseConnectionOrThrowError) { + debugLog('Getting connection after open'); + db = await sqlitePlugin.getDatabaseConnectionOrThrowError(connectionOptions.database); + } + + // Verify the database exists + const exists = await sqlitePlugin.isDBExists({ + database: connectionOptions.database, + path: fullDbPath + }); + + debugLog('Database exists check:', { + exists, + path: fullDbPath, + fileExists: fs.existsSync(fullDbPath) + }); + + // If database doesn't exist, try to create it + if (!exists) { + debugLog('Database does not exist, attempting to create...'); + // Create an empty file to ensure the directory is writable + await fs.promises.writeFile(fullDbPath, ''); + + // Try opening again + await sqlitePlugin.open({ + database: connectionOptions.database, + path: fullDbPath, + version: connectionOptions.version, + readOnly: false, // Force read-write for creation + encryption: connectionOptions.encryption, + useNative: connectionOptions.useNative, + mode: 'rwc' // Force create mode + }); + + // Verify creation + const created = await sqlitePlugin.isDBExists({ + database: connectionOptions.database, + path: fullDbPath + }); + + debugLog('Database creation result:', { + created, + path: fullDbPath, + fileExists: fs.existsSync(fullDbPath), + fileSize: fs.existsSync(fullDbPath) ? fs.statSync(fullDbPath).size : 0 + }); + } + + // Get final connection + if (sqlitePlugin.getDatabaseConnectionOrThrowError) { + debugLog('Getting final connection'); + db = await sqlitePlugin.getDatabaseConnectionOrThrowError(connectionOptions.database); + } + + debugLog('Final connection state:', { + hasConnection: !!db, + type: typeof db, + isNull: db === null, + isUndefined: db === undefined, + keys: db ? Object.keys(db) : null, + methods: db ? Object.getOwnPropertyNames(Object.getPrototypeOf(db)) : null, + prototype: db ? Object.getPrototypeOf(db)?.constructor?.name : null + }); + + } catch (openError) { + debugLog('Error during open/create:', { + name: openError.name, + message: openError.message, + stack: openError.stack, + code: (openError as any).code, + errno: (openError as any).errno, + syscall: (openError as any).syscall + }); + throw openError; + } + } else { + throw new Error('No valid createConnection method found on prototype'); } - logger.info('Database connection verified successfully'); } catch (error) { - throw new Error(`Database connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + debugLog('createConnection/open error:', { + name: error.name, + message: error.message, + stack: error.stack, + code: (error as any).code, + errno: (error as any).errno, + syscall: (error as any).syscall + }); + throw error; } + if (!db || typeof db !== 'object') { + debugLog('Invalid database connection response:', { + value: db, + type: typeof db, + isNull: db === null, + isUndefined: db === undefined + }); + throw new Error(`Failed to create database connection - invalid response. Path: ${fullDbPath}`); + } + + // Verify connection state + debugLog('Verifying connection state...'); + try { + const isOpen = await db.isDBOpen(); + debugLog('Connection state:', { + isOpen, + methods: Object.keys(db), + prototype: Object.getOwnPropertyNames(Object.getPrototypeOf(db)) + }); + } catch (error) { + debugLog('Error checking connection state:', error); + } + sqliteInitialized = true; - logger.info('SQLite plugin initialization completed successfully'); + debugLog('SQLite plugin initialization completed successfully'); } catch (error) { logger.error('SQLite plugin initialization failed:', error); - // Store the error but don't throw initializationError = error instanceof Error ? error : new Error(String(error)); - // Reset state on failure but allow app to continue sqlitePlugin = null; sqliteInitialized = false; } finally { @@ -291,18 +584,22 @@ export function setupSQLiteHandlers(): void { throw new Error('Database path not initialized'); } - // Use modern plugin conventions for connection options + // Use same connection options format const connectionOptions = { ...options, - database: 'timesafariSQLite', + database: 'timesafari', // Base name only readOnly: false, mode: 'rwc', encryption: 'no-encryption', useNative: true, - location: dbDir // Use path relative to electron folder + location: 'default' // Let plugin handle path resolution }; - logger.info('Creating database connection with options:', connectionOptions); + logger.info('Creating database connection with options:', { + ...connectionOptions, + expectedBehavior: 'Plugin will append SQLite suffix and handle path resolution' + }); + const result = await sqlitePlugin.createConnection(connectionOptions); if (!result || typeof result !== 'object') {