|
|
@ -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<string, unknown>[]; |
|
|
|
} |
|
|
|
|
|
|
|
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<string, unknown> |
|
|
|
) { |
|
|
|
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<string, unknown>): 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<void> { |
|
|
|
})) |
|
|
|
}); |
|
|
|
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() |
|
|
|
}); |
|
|
|
} |