diff --git a/electron/src/rt/sqlite-init.ts b/electron/src/rt/sqlite-init.ts index 87b39653..579d690f 100644 --- a/electron/src/rt/sqlite-init.ts +++ b/electron/src/rt/sqlite-init.ts @@ -92,34 +92,129 @@ const MAX_RECOVERY_ATTEMPTS = 3; const RECOVERY_DELAY_MS = 1000; const VERIFICATION_TIMEOUT_MS = 5000; -// Error handling +// Type definitions for SQLite operations +interface SQLiteConnectionOptions { + database: string; + version?: number; + readOnly?: boolean; + readonly?: boolean; + encryption?: string; + mode?: string; + useNative?: boolean; + [key: string]: unknown; +} + +interface SQLiteQueryOptions { + statement: string; + values?: unknown[]; +} + +interface SQLiteExecuteOptions { + statements: SQLiteQueryOptions[]; + transaction?: boolean; +} + +interface SQLiteResult { + changes?: { changes: number; lastId?: number }; + values?: Record[]; +} + +interface SQLiteEchoResult { + value: string; +} + +// Enhanced error types class SQLiteError extends Error { constructor( message: string, public context: string, - public originalError?: unknown + public code: string = 'SQLITE_ERROR', + public originalError?: unknown, + public details?: Record ) { super(message); this.name = 'SQLiteError'; } + + toJSON() { + return { + name: this.name, + message: this.message, + context: this.context, + code: this.code, + details: this.details, + stack: this.stack, + originalError: this.originalError instanceof Error ? { + name: this.originalError.name, + message: this.originalError.message, + stack: this.originalError.stack + } : this.originalError + }; + } } -const handleError = (error: unknown, context: string): SQLiteError => { - const errorMessage = error instanceof Error - ? error.message - : 'Unknown error occurred'; - const errorStack = error instanceof Error - ? error.stack - : undefined; - +// Validation utilities +const validateConnectionOptions = (options: unknown): SQLiteConnectionOptions => { + if (!options || typeof options !== 'object') { + throw new SQLiteError('Invalid connection options', 'validation', 'SQLITE_INVALID_OPTIONS'); + } + + const opts = options as SQLiteConnectionOptions; + if (!opts.database || typeof opts.database !== 'string') { + throw new SQLiteError('Database name is required', 'validation', 'SQLITE_INVALID_DATABASE'); + } + + return opts; +}; + +const validateQueryOptions = (options: unknown): SQLiteQueryOptions => { + if (!options || typeof options !== 'object') { + throw new SQLiteError('Invalid query options', 'validation', 'SQLITE_INVALID_OPTIONS'); + } + + const opts = options as SQLiteQueryOptions; + if (!opts.statement || typeof opts.statement !== 'string') { + throw new SQLiteError('SQL statement is required', 'validation', 'SQLITE_INVALID_STATEMENT'); + } + + if (opts.values && !Array.isArray(opts.values)) { + throw new SQLiteError('Values must be an array', 'validation', 'SQLITE_INVALID_VALUES'); + } + + return opts; +}; + +// Enhanced error handler with more context +const handleError = (error: unknown, context: string, details?: Record): SQLiteError => { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + const errorStack = error instanceof Error ? error.stack : undefined; + + // Determine error code based on context and message + let code = 'SQLITE_ERROR'; + if (errorMessage.includes('database is locked')) { + code = 'SQLITE_BUSY'; + } else if (errorMessage.includes('no such table')) { + code = 'SQLITE_NO_TABLE'; + } else if (errorMessage.includes('syntax error')) { + code = 'SQLITE_SYNTAX_ERROR'; + } + logger.error(`Error in ${context}:`, { message: errorMessage, stack: errorStack, context, + code, + details, timestamp: new Date().toISOString() }); - return new SQLiteError(`${context} failed: ${errorMessage}`, context, error); + return new SQLiteError( + `${context} failed: ${errorMessage}`, + context, + code, + error, + details + ); }; // Add delay utility with timeout @@ -472,8 +567,9 @@ export async function initializeSQLite(): Promise { })) }); throw new SQLiteError( - 'Database migrations failed', + `Database migrations failed: ${failedMigrations.map(f => f.name).join(', ')}`, 'initializeSQLite', + 'SQLITE_MIGRATION_FAILED', failedMigrations ); } @@ -555,6 +651,69 @@ export function setupSQLiteHandlers(): void { return false; } }); + + // Enhanced sqlite-echo handler + ipcMain.handle('sqlite-echo', async (_event, options) => { + try { + if (!await verifyPluginState()) { + throw new SQLiteError( + 'Plugin not available', + 'sqlite-echo', + 'SQLITE_PLUGIN_UNAVAILABLE', + null, + { pluginState } + ); + } + + if (!options || typeof options !== 'object' || !('value' in options)) { + throw new SQLiteError( + 'Invalid echo options', + 'sqlite-echo', + 'SQLITE_INVALID_OPTIONS', + null, + { options } + ); + } + + const { value } = options as { value: unknown }; + if (typeof value !== 'string') { + throw new SQLiteError( + 'Echo value must be a string', + 'sqlite-echo', + 'SQLITE_INVALID_VALUE', + null, + { value, type: typeof value } + ); + } + + logger.debug('Echo test:', { value, timestamp: new Date().toISOString() }); + const result = await pluginState.instance.echo({ value }); + + if (!result || typeof result !== 'object' || !('value' in result)) { + throw new SQLiteError( + 'Invalid echo result', + 'sqlite-echo', + 'SQLITE_INVALID_RESULT', + null, + { result } + ); + } + + if (result.value !== value) { + throw new SQLiteError( + 'Echo test failed - value mismatch', + 'sqlite-echo', + 'SQLITE_ECHO_MISMATCH', + null, + { expected: value, received: result.value } + ); + } + + return result as SQLiteEchoResult; + } catch (error) { + throw handleError(error, 'sqlite-echo', { options }); + } + }); ipcMain.handle('sqlite-get-error', async () => { return pluginState.lastError ? { @@ -579,69 +738,114 @@ export function setupSQLiteHandlers(): void { } }); - // Add the sqlite-run handler + // Enhanced sqlite-run handler ipcMain.handle('sqlite-run', async (_event, options) => { try { if (!await verifyPluginState()) { - throw new SQLiteError('Plugin not available', 'sqlite-run'); - } - - const { statement, values } = options; - if (!statement) { - throw new SQLiteError('No statement provided', 'sqlite-run'); + throw new SQLiteError( + 'Plugin not available', + 'sqlite-run', + 'SQLITE_PLUGIN_UNAVAILABLE', + null, + { pluginState } + ); } - // Add database name to options - const runOptions = { - database: 'timesafari', // Use the same database name as in initialization - statement, - values: values || [] + const runOptions = validateQueryOptions(options); + const runWithDb = { + database: 'timesafari', + ...runOptions }; - logger.debug('Running SQL statement:', runOptions); - const result = await pluginState.instance.run(runOptions); + logger.debug('Running SQL statement:', { + ...runWithDb, + timestamp: new Date().toISOString() + }); + + const result = await pluginState.instance.run(runWithDb); if (!result) { - throw new SQLiteError('Run operation returned no result', 'sqlite-run'); + throw new SQLiteError( + 'Run operation returned no result', + 'sqlite-run', + 'SQLITE_NO_RESULT', + null, + { run: runWithDb } + ); + } + + // Validate result structure + if (!('changes' in result)) { + throw new SQLiteError( + 'Invalid run result structure', + 'sqlite-run', + 'SQLITE_INVALID_RESULT', + null, + { result } + ); } - return result; + return result as SQLiteResult; } catch (error) { - throw handleError(error, 'sqlite-run'); + throw handleError(error, 'sqlite-run', { options }); } }); - // Add the sqlite-query handler + // Enhanced sqlite-query handler ipcMain.handle('sqlite-query', async (_event, options) => { try { if (!await verifyPluginState()) { - throw new SQLiteError('Plugin not available', 'sqlite-query'); + throw new SQLiteError( + 'Plugin not available', + 'sqlite-query', + 'SQLITE_PLUGIN_UNAVAILABLE', + null, + { pluginState } + ); } - const { statement, values } = options; - if (!statement) { - throw new SQLiteError('No statement provided', 'sqlite-query'); - } - - // Add database name to options - const queryOptions = { - database: 'timesafari', // Use the same database name as in initialization - statement, - values: values || [] + const queryOptions = validateQueryOptions(options); + const queryWithDb = { + database: 'timesafari', + ...queryOptions }; - logger.debug('Executing SQL query:', queryOptions); - const result = await pluginState.instance.query(queryOptions); + logger.debug('Executing SQL query:', { + ...queryWithDb, + timestamp: new Date().toISOString() + }); + + const result = await pluginState.instance.query(queryWithDb); if (!result) { - throw new SQLiteError('Query operation returned no result', 'sqlite-query'); + throw new SQLiteError( + 'Query operation returned no result', + 'sqlite-query', + 'SQLITE_NO_RESULT', + null, + { query: queryWithDb } + ); } - return result; + // Validate result structure + if (!('values' in result) && !('changes' in result)) { + throw new SQLiteError( + 'Invalid query result structure', + 'sqlite-query', + 'SQLITE_INVALID_RESULT', + null, + { result } + ); + } + + return result as SQLiteResult; } catch (error) { - throw handleError(error, 'sqlite-query'); + throw handleError(error, 'sqlite-query', { options }); } }); - logger.info('SQLite IPC handlers registered successfully'); + logger.info('SQLite IPC handlers registered successfully', { + handlers: handlers.join(', '), + timestamp: new Date().toISOString() + }); } \ No newline at end of file diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 5892bfaa..e2f49a18 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -13,9 +13,16 @@ interface ElectronAPI { [key: string]: unknown; }) => Promise; closeConnection: (options: { database: string }) => Promise; - query: (options: { statement: string; values?: unknown[] }) => Promise; - run: (options: { statement: string; values?: unknown[] }) => Promise; - execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise; + query: (options: { statement: string; values?: unknown[] }) => Promise<{ + values?: Record[]; + changes?: { changes: number; lastId?: number }; + }>; + run: (options: { statement: string; values?: unknown[] }) => Promise<{ + changes?: { changes: number; lastId?: number }; + }>; + execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<{ + changes?: { changes: number; lastId?: number }[]; + }>; getPlatform: () => Promise; }; ipcRenderer: { @@ -34,7 +41,46 @@ interface ElectronAPI { declare global { interface Window { - electron: ElectronAPI; + electron: { + ipcRenderer: { + on: (channel: string, func: (...args: unknown[]) => void) => void; + once: (channel: string, func: (...args: unknown[]) => void) => void; + send: (channel: string, data: unknown) => void; + invoke: (channel: string, ...args: unknown[]) => Promise; + }; + sqlite: { + isAvailable: () => Promise; + echo: (value: string) => Promise<{ value: string }>; + createConnection: (options: { + database: string; + version?: number; + readOnly?: boolean; + readonly?: boolean; + encryption?: string; + mode?: string; + useNative?: boolean; + [key: string]: unknown; + }) => Promise; + closeConnection: (options: { database: string }) => Promise; + query: (options: { statement: string; values?: unknown[] }) => Promise<{ + values?: Record[]; + changes?: { changes: number; lastId?: number }; + }>; + run: (options: { statement: string; values?: unknown[] }) => Promise<{ + changes?: { changes: number; lastId?: number }; + }>; + execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<{ + changes?: { changes: number; lastId?: number }[]; + }>; + getPlatform: () => Promise; + }; + env: { + platform: string; + isDev: boolean; + }; + getPath: (pathType: string) => Promise; + getBasePath: () => Promise; + }; } }