- Add Winston-based structured logging system with: - Separate console and file output formats - Custom SQLite and migration loggers - Configurable log levels and verbosity - Log rotation and file management - Type-safe logger extensions - Improve IPC handler management: - Add handler registration tracking - Implement proper cleanup before re-registration - Fix handler registration conflicts - Add better error handling for IPC operations - Add migration logging controls: - Configurable via DEBUG_MIGRATIONS env var - Reduced console noise while maintaining file logs - Structured migration status reporting Security: - Add proper log file permissions (0o755) - Implement log rotation to prevent disk space issues - Add type safety for all logging operations - Prevent handler registration conflicts Dependencies: - Add winston for enhanced logging - Remove deprecated @types/winston This change improves debugging capabilities while reducing console noise and fixing IPC handler registration issues that could cause database operation failures.
815 lines
24 KiB
TypeScript
815 lines
24 KiB
TypeScript
/**
|
|
* SQLite Initialization and Management for TimeSafari Electron
|
|
*
|
|
* This module handles the complete lifecycle of SQLite database initialization,
|
|
* connection management, and IPC communication in the TimeSafari Electron app.
|
|
*
|
|
* Key Features:
|
|
* - Database path management with proper permissions
|
|
* - Plugin initialization and state verification
|
|
* - Connection lifecycle management
|
|
* - PRAGMA configuration for optimal performance
|
|
* - Migration system integration with configurable logging
|
|
* - Error handling and recovery
|
|
* - IPC communication layer with proper cleanup
|
|
*
|
|
* Database Configuration:
|
|
* - Uses WAL journal mode for better concurrency
|
|
* - Configures optimal PRAGMA settings
|
|
* - Implements connection pooling
|
|
* - Handles encryption (when enabled)
|
|
*
|
|
* State Management:
|
|
* - Tracks plugin initialization state
|
|
* - Monitors connection health
|
|
* - Manages transaction state
|
|
* - Implements recovery mechanisms
|
|
*
|
|
* Error Handling:
|
|
* - Custom SQLiteError class for detailed error tracking
|
|
* - Comprehensive error logging
|
|
* - Automatic recovery attempts
|
|
* - State verification before operations
|
|
*
|
|
* Security:
|
|
* - Proper file permissions (0o755)
|
|
* - Write access verification
|
|
* - Connection state validation
|
|
* - Transaction safety
|
|
*
|
|
* Performance:
|
|
* - WAL mode for better concurrency
|
|
* - Optimized PRAGMA settings
|
|
* - Connection pooling
|
|
* - Efficient state management
|
|
*
|
|
* @author Matthew Raymer <matthew.raymer@anomalistdesign.com>
|
|
* @version 1.0.0
|
|
* @since 2025-06-01
|
|
*/
|
|
|
|
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';
|
|
import { runMigrations } from './sqlite-migrations';
|
|
import { logger } from './logger';
|
|
import { startDatabaseOperation, endDatabaseOperation } from '../setup';
|
|
|
|
// Types for state management
|
|
interface PluginState {
|
|
isInitialized: boolean;
|
|
isAvailable: boolean;
|
|
lastVerified: Date | null;
|
|
lastError: Error | null;
|
|
instance: any | null;
|
|
}
|
|
|
|
interface TransactionState {
|
|
isActive: boolean;
|
|
lastVerified: Date | null;
|
|
database: string | null;
|
|
}
|
|
|
|
// State tracking
|
|
let pluginState: PluginState = {
|
|
isInitialized: false,
|
|
isAvailable: false,
|
|
lastVerified: null,
|
|
lastError: null,
|
|
instance: null
|
|
};
|
|
|
|
let transactionState: TransactionState = {
|
|
isActive: false,
|
|
lastVerified: null,
|
|
database: null
|
|
};
|
|
|
|
// Constants
|
|
const MAX_RECOVERY_ATTEMPTS = 3;
|
|
const RECOVERY_DELAY_MS = 1000;
|
|
const VERIFICATION_TIMEOUT_MS = 5000;
|
|
|
|
// 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 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
|
|
};
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
code,
|
|
error,
|
|
details
|
|
);
|
|
};
|
|
|
|
// Add delay utility with timeout
|
|
const delay = (ms: number, timeoutMs: number = VERIFICATION_TIMEOUT_MS): Promise<void> => {
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
reject(new SQLiteError('Operation timed out', 'delay'));
|
|
}, timeoutMs);
|
|
|
|
setTimeout(() => {
|
|
clearTimeout(timeout);
|
|
resolve();
|
|
}, ms);
|
|
});
|
|
};
|
|
|
|
// Plugin state verification
|
|
const verifyPluginState = async (): Promise<boolean> => {
|
|
if (!pluginState.instance || !pluginState.isInitialized) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Test plugin responsiveness
|
|
const echoResult = await pluginState.instance.echo({ value: 'test' });
|
|
if (!echoResult || echoResult.value !== 'test') {
|
|
throw new SQLiteError('Plugin echo test failed', 'verifyPluginState');
|
|
}
|
|
|
|
pluginState.isAvailable = true;
|
|
pluginState.lastVerified = new Date();
|
|
pluginState.lastError = null;
|
|
|
|
return true;
|
|
} catch (error) {
|
|
pluginState.isAvailable = false;
|
|
pluginState.lastError = handleError(error, 'verifyPluginState');
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Transaction state verification
|
|
const verifyTransactionState = async (database: string): Promise<boolean> => {
|
|
if (!pluginState.instance || !pluginState.isAvailable) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Check if we're in a transaction
|
|
const isActive = await pluginState.instance.isTransactionActive({ database });
|
|
|
|
transactionState.isActive = isActive;
|
|
transactionState.lastVerified = new Date();
|
|
transactionState.database = database;
|
|
|
|
return true;
|
|
} catch (error) {
|
|
transactionState.isActive = false;
|
|
transactionState.lastVerified = new Date();
|
|
transactionState.database = null;
|
|
|
|
logger.error('Transaction state verification failed:', error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Plugin initialization
|
|
const initializePlugin = async (): Promise<boolean> => {
|
|
logger.info('Starting plugin initialization');
|
|
|
|
try {
|
|
// Create plugin instance
|
|
let rawPlugin;
|
|
if (SQLiteModule.default?.CapacitorSQLite) {
|
|
logger.debug('Using default export CapacitorSQLite');
|
|
rawPlugin = new SQLiteModule.default.CapacitorSQLite();
|
|
} else {
|
|
logger.debug('Using direct CapacitorSQLite class');
|
|
rawPlugin = new CapacitorSQLite();
|
|
}
|
|
|
|
// Verify instance
|
|
if (!rawPlugin || typeof rawPlugin !== 'object') {
|
|
throw new SQLiteError('Invalid plugin instance created', 'initializePlugin');
|
|
}
|
|
|
|
// Test plugin functionality
|
|
const echoResult = await rawPlugin.echo({ value: 'test' });
|
|
if (!echoResult || echoResult.value !== 'test') {
|
|
throw new SQLiteError('Plugin echo test failed', 'initializePlugin');
|
|
}
|
|
|
|
// Update state only after successful verification
|
|
pluginState = {
|
|
isInitialized: true,
|
|
isAvailable: true,
|
|
lastVerified: new Date(),
|
|
lastError: null,
|
|
instance: rawPlugin
|
|
};
|
|
|
|
logger.info('Plugin initialized successfully');
|
|
return true;
|
|
} catch (error) {
|
|
pluginState = {
|
|
isInitialized: false,
|
|
isAvailable: false,
|
|
lastVerified: new Date(),
|
|
lastError: handleError(error, 'initializePlugin'),
|
|
instance: null
|
|
};
|
|
|
|
logger.error('Plugin initialization failed:', {
|
|
error: pluginState.lastError,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Recovery mechanism
|
|
const recoverPluginState = async (attempt: number = 1): Promise<boolean> => {
|
|
logger.info(`Attempting plugin state recovery (attempt ${attempt}/${MAX_RECOVERY_ATTEMPTS})`);
|
|
|
|
if (attempt > MAX_RECOVERY_ATTEMPTS) {
|
|
logger.error('Max recovery attempts reached');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Cleanup existing connection if any
|
|
if (pluginState.instance) {
|
|
try {
|
|
await pluginState.instance.closeConnection({ database: 'timesafari' });
|
|
logger.debug('Closed existing database connection during recovery');
|
|
} catch (error) {
|
|
logger.warn('Error closing connection during recovery:', error);
|
|
}
|
|
}
|
|
|
|
// Reset state
|
|
pluginState = {
|
|
isInitialized: false,
|
|
isAvailable: false,
|
|
lastVerified: new Date(),
|
|
lastError: null,
|
|
instance: null
|
|
};
|
|
|
|
// Wait before retry with exponential backoff
|
|
const backoffDelay = RECOVERY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
await delay(backoffDelay);
|
|
|
|
// Reinitialize
|
|
const success = await initializePlugin();
|
|
if (!success && attempt < MAX_RECOVERY_ATTEMPTS) {
|
|
return recoverPluginState(attempt + 1);
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
logger.error('Plugin recovery failed:', error);
|
|
if (attempt < MAX_RECOVERY_ATTEMPTS) {
|
|
return recoverPluginState(attempt + 1);
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Initializes database paths and ensures proper permissions
|
|
*
|
|
* This function:
|
|
* 1. Creates the database directory if it doesn't exist
|
|
* 2. Sets proper permissions (0o755)
|
|
* 3. Verifies write access
|
|
* 4. Returns the absolute path to the database directory
|
|
*
|
|
* @returns {Promise<string>} Absolute path to database directory
|
|
* @throws {SQLiteError} If directory creation or permission setting fails
|
|
*/
|
|
const initializeDatabasePaths = async (): Promise<string> => {
|
|
try {
|
|
// Get the absolute app data directory
|
|
const appDataDir = path.join(os.homedir(), 'Databases', 'TimeSafari');
|
|
logger.info('App data directory:', appDataDir);
|
|
|
|
// Ensure directory exists with proper permissions
|
|
if (!fs.existsSync(appDataDir)) {
|
|
await fs.promises.mkdir(appDataDir, {
|
|
recursive: true,
|
|
mode: 0o755
|
|
});
|
|
} else {
|
|
await fs.promises.chmod(appDataDir, 0o755);
|
|
}
|
|
|
|
// Verify directory permissions
|
|
const stats = await fs.promises.stat(appDataDir);
|
|
logger.info('Directory permissions:', {
|
|
mode: stats.mode.toString(8),
|
|
uid: stats.uid,
|
|
gid: stats.gid,
|
|
isDirectory: stats.isDirectory(),
|
|
isWritable: !!(stats.mode & 0o200)
|
|
});
|
|
|
|
// Test write access
|
|
const testFile = path.join(appDataDir, '.write-test');
|
|
await fs.promises.writeFile(testFile, 'test');
|
|
await fs.promises.unlink(testFile);
|
|
|
|
return appDataDir;
|
|
} catch (error) {
|
|
throw handleError(error, 'initializeDatabasePaths');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Main SQLite initialization function
|
|
*
|
|
* Orchestrates the complete database initialization process:
|
|
* 1. Sets up database paths
|
|
* 2. Initializes the SQLite plugin
|
|
* 3. Creates and verifies database connection
|
|
* 4. Configures database PRAGMAs
|
|
* 5. Runs database migrations
|
|
* 6. Handles errors and recovery
|
|
*
|
|
* Database Configuration:
|
|
* - Uses WAL journal mode
|
|
* - Enables foreign keys
|
|
* - Sets optimal page size and cache
|
|
* - Configures busy timeout
|
|
*
|
|
* Error Recovery:
|
|
* - Implements exponential backoff
|
|
* - Verifies plugin state
|
|
* - Attempts connection recovery
|
|
* - Maintains detailed error logs
|
|
*
|
|
* @throws {SQLiteError} If initialization fails and recovery is unsuccessful
|
|
*/
|
|
export async function initializeSQLite(): Promise<void> {
|
|
logger.info('Starting SQLite initialization');
|
|
|
|
try {
|
|
// Initialize database paths
|
|
const dbDir = await initializeDatabasePaths();
|
|
const dbPath = path.join(dbDir, 'timesafariSQLite.db');
|
|
|
|
// Initialize plugin
|
|
if (!await initializePlugin()) {
|
|
throw new SQLiteError('Plugin initialization failed', 'initializeSQLite');
|
|
}
|
|
|
|
// Verify plugin state
|
|
if (!await verifyPluginState()) {
|
|
throw new SQLiteError('Plugin state verification failed', 'initializeSQLite');
|
|
}
|
|
|
|
// Set up database connection
|
|
const connectionOptions = {
|
|
database: 'timesafari',
|
|
version: 1,
|
|
readOnly: false,
|
|
encryption: 'no-encryption',
|
|
useNative: true,
|
|
mode: 'rwc'
|
|
};
|
|
|
|
// Create and verify connection
|
|
logger.debug('Creating database connection:', connectionOptions);
|
|
await pluginState.instance.createConnection(connectionOptions);
|
|
await delay(500); // Wait for connection registration
|
|
|
|
const isRegistered = await pluginState.instance.isDatabase({
|
|
database: connectionOptions.database
|
|
});
|
|
|
|
if (!isRegistered) {
|
|
throw new SQLiteError('Database not registered', 'initializeSQLite');
|
|
}
|
|
|
|
// Open database
|
|
logger.debug('Opening database with options:', connectionOptions);
|
|
await pluginState.instance.open({
|
|
...connectionOptions,
|
|
mode: 'rwc'
|
|
});
|
|
|
|
// Set PRAGMAs with detailed logging
|
|
const pragmaStatements = [
|
|
'PRAGMA foreign_keys = ON;',
|
|
'PRAGMA journal_mode = WAL;', // Changed to WAL for better concurrency
|
|
'PRAGMA synchronous = NORMAL;',
|
|
'PRAGMA temp_store = MEMORY;',
|
|
'PRAGMA page_size = 4096;',
|
|
'PRAGMA cache_size = 2000;',
|
|
'PRAGMA busy_timeout = 15000;', // Increased to 15 seconds
|
|
'PRAGMA wal_autocheckpoint = 1000;' // Added WAL checkpoint setting
|
|
];
|
|
|
|
logger.debug('Setting database PRAGMAs');
|
|
for (const statement of pragmaStatements) {
|
|
try {
|
|
logger.debug('Executing PRAGMA:', statement);
|
|
const result = await pluginState.instance.execute({
|
|
database: connectionOptions.database,
|
|
statements: statement,
|
|
transaction: false
|
|
});
|
|
logger.debug('PRAGMA result:', { statement, result });
|
|
} catch (error) {
|
|
logger.error('PRAGMA execution failed:', {
|
|
statement,
|
|
error: error instanceof Error ? {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
name: error.name
|
|
} : error
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Run migrations with enhanced error logging
|
|
logger.info('Starting database migrations');
|
|
const migrationResults = await runMigrations(
|
|
pluginState.instance,
|
|
connectionOptions.database
|
|
);
|
|
|
|
// Check migration results with detailed logging
|
|
const failedMigrations = migrationResults.filter(r => !r.success);
|
|
if (failedMigrations.length > 0) {
|
|
logger.error('Migration failures:', {
|
|
totalMigrations: migrationResults.length,
|
|
failedCount: failedMigrations.length,
|
|
failures: failedMigrations.map(f => ({
|
|
version: f.version,
|
|
name: f.name,
|
|
error: f.error instanceof Error ? {
|
|
message: f.error.message,
|
|
stack: f.error.stack,
|
|
name: f.error.name
|
|
} : f.error,
|
|
state: f.state
|
|
}))
|
|
});
|
|
throw new SQLiteError(
|
|
`Database migrations failed: ${failedMigrations.map(f => f.name).join(', ')}`,
|
|
'initializeSQLite',
|
|
'SQLITE_MIGRATION_FAILED',
|
|
failedMigrations
|
|
);
|
|
}
|
|
|
|
logger.info('SQLite initialization completed successfully');
|
|
} catch (error) {
|
|
const sqliteError = handleError(error, 'initializeSQLite');
|
|
logger.error('SQLite initialization failed:', {
|
|
error: sqliteError,
|
|
pluginState: {
|
|
isInitialized: pluginState.isInitialized,
|
|
isAvailable: pluginState.isAvailable,
|
|
lastVerified: pluginState.lastVerified,
|
|
lastError: pluginState.lastError
|
|
}
|
|
});
|
|
|
|
// Attempt recovery
|
|
if (await recoverPluginState()) {
|
|
logger.info('Recovery successful, retrying initialization');
|
|
return initializeSQLite();
|
|
}
|
|
|
|
throw sqliteError;
|
|
}
|
|
}
|
|
|
|
// Add IPC handler tracking
|
|
const registeredHandlers = new Set<string>();
|
|
|
|
/**
|
|
* Removes all registered SQLite IPC handlers
|
|
* Called before re-registering handlers to prevent duplicates
|
|
*/
|
|
const cleanupSQLiteHandlers = (): void => {
|
|
logger.debug('Cleaning up SQLite IPC handlers');
|
|
for (const channel of registeredHandlers) {
|
|
try {
|
|
ipcMain.removeHandler(channel);
|
|
logger.debug(`Removed handler for channel: ${channel}`);
|
|
} catch (error) {
|
|
logger.warn(`Failed to remove handler for channel ${channel}:`, error);
|
|
}
|
|
}
|
|
registeredHandlers.clear();
|
|
};
|
|
|
|
/**
|
|
* Registers an IPC handler with tracking
|
|
* @param channel The IPC channel to register
|
|
* @param handler The handler function
|
|
*/
|
|
const registerHandler = (channel: string, handler: (...args: any[]) => Promise<any>): void => {
|
|
if (registeredHandlers.has(channel)) {
|
|
logger.debug(`Handler already registered for channel: ${channel}, removing first`);
|
|
ipcMain.removeHandler(channel);
|
|
}
|
|
ipcMain.handle(channel, handler);
|
|
registeredHandlers.add(channel);
|
|
logger.debug(`Registered handler for channel: ${channel}`);
|
|
};
|
|
|
|
/**
|
|
* Sets up IPC handlers for SQLite operations
|
|
* Registers handlers for all SQLite operations defined in VALID_CHANNELS.invoke
|
|
* Tracks database operations to prevent reloads during critical operations
|
|
* Implements proper handler cleanup to prevent duplicates
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
export function setupSQLiteHandlers(): void {
|
|
logger.info('Setting up SQLite IPC handlers');
|
|
|
|
// Clean up any existing handlers first
|
|
cleanupSQLiteHandlers();
|
|
|
|
// Handler for checking SQLite availability
|
|
registerHandler('sqlite-is-available', async () => {
|
|
logger.debug('Checking SQLite availability');
|
|
try {
|
|
startDatabaseOperation();
|
|
const isAvailable = await verifyPluginState();
|
|
logger.debug('SQLite availability check:', { isAvailable });
|
|
return isAvailable;
|
|
} catch (error) {
|
|
logger.error('SQLite availability check failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for echo test
|
|
registerHandler('sqlite-echo', async (_event, { value }) => {
|
|
logger.debug('SQLite echo test:', { value });
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-echo');
|
|
}
|
|
const result = await pluginState.instance.echo({ value });
|
|
logger.debug('SQLite echo result:', result);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('SQLite echo failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for creating database connection
|
|
registerHandler('sqlite-create-connection', async (_event, options: SQLiteConnectionOptions) => {
|
|
logger.debug('Creating SQLite connection:', options);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-create-connection');
|
|
}
|
|
const validatedOptions = validateConnectionOptions(options);
|
|
await pluginState.instance.createConnection(validatedOptions);
|
|
logger.debug('SQLite connection created successfully');
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('SQLite connection creation failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for executing SQL statements
|
|
registerHandler('sqlite-execute', async (_event, options: SQLiteExecuteOptions) => {
|
|
logger.debug('Executing SQL statements:', options);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-execute');
|
|
}
|
|
const result = await pluginState.instance.execute(options);
|
|
logger.debug('SQL execution result:', result);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('SQL execution failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for querying data
|
|
registerHandler('sqlite-query', async (_event, options: SQLiteQueryOptions) => {
|
|
logger.debug('Querying SQLite:', options);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-query');
|
|
}
|
|
const result = await pluginState.instance.query(options);
|
|
logger.debug('SQL query result:', result);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('SQL query failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for running SQL statements
|
|
registerHandler('sqlite-run', async (_event, options: SQLiteQueryOptions) => {
|
|
logger.debug('Running SQL statement:', options);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-run');
|
|
}
|
|
const result = await pluginState.instance.run(options);
|
|
logger.debug('SQL run result:', result);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('SQL run failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for closing database connection
|
|
registerHandler('sqlite-close-connection', async (_event, options: { database: string }) => {
|
|
logger.debug('Closing SQLite connection:', options);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-close-connection');
|
|
}
|
|
await pluginState.instance.closeConnection(options);
|
|
logger.debug('SQLite connection closed successfully');
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('SQLite connection close failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for getting database path
|
|
registerHandler('get-path', async () => {
|
|
logger.debug('Getting database path');
|
|
try {
|
|
startDatabaseOperation();
|
|
const dbDir = await initializeDatabasePaths();
|
|
const dbPath = path.join(dbDir, 'timesafariSQLite.db');
|
|
logger.debug('Database path:', dbPath);
|
|
return dbPath;
|
|
} catch (error) {
|
|
logger.error('Failed to get database path:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for getting base path
|
|
registerHandler('get-base-path', async () => {
|
|
logger.debug('Getting base path');
|
|
try {
|
|
startDatabaseOperation();
|
|
const dbDir = await initializeDatabasePaths();
|
|
logger.debug('Base path:', dbDir);
|
|
return dbDir;
|
|
} catch (error) {
|
|
logger.error('Failed to get base path:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
logger.info('SQLite IPC handlers setup complete');
|
|
}
|