You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
899 lines
27 KiB
899 lines
27 KiB
/**
|
|
* 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;
|
|
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 - simplified to match sacred-sql
|
|
const connectionOptions = {
|
|
database: 'timesafari',
|
|
version: 1,
|
|
readOnly: false,
|
|
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 to match sacred-sql approach
|
|
const pragmaStatements = [
|
|
'PRAGMA journal_mode = MEMORY;', // Changed to MEMORY mode to match sacred-sql
|
|
'PRAGMA foreign_keys = ON;',
|
|
'PRAGMA synchronous = NORMAL;',
|
|
'PRAGMA temp_store = MEMORY;',
|
|
'PRAGMA page_size = 4096;',
|
|
'PRAGMA cache_size = 2000;'
|
|
// Removed WAL-specific settings
|
|
];
|
|
|
|
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 opening database
|
|
registerHandler('sqlite-open', async (_event, options: SQLiteConnectionOptions) => {
|
|
logger.debug('Opening SQLite database:', options);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-open');
|
|
}
|
|
await pluginState.instance.open(options);
|
|
logger.debug('SQLite database opened successfully');
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('SQLite database open failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for closing database
|
|
registerHandler('sqlite-close', async (_event, options: { database: string }) => {
|
|
logger.debug('Closing SQLite database:', options);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-close');
|
|
}
|
|
await pluginState.instance.close(options);
|
|
logger.debug('SQLite database closed successfully');
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('SQLite database close failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
// Handler for checking if database is open
|
|
registerHandler('sqlite-is-db-open', async (_event, options: { database: string }) => {
|
|
logger.debug('Checking if SQLite database is open:', options);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-is-db-open');
|
|
}
|
|
const isOpen = await pluginState.instance.isDBOpen(options);
|
|
logger.debug('SQLite database open check:', { isOpen });
|
|
return isOpen;
|
|
} catch (error) {
|
|
logger.error('SQLite database open check 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();
|
|
}
|
|
});
|
|
|
|
// Handler for SQLite status updates
|
|
registerHandler('sqlite-status', async (_event, status: { status: string; database: string; timestamp: number }) => {
|
|
logger.debug('SQLite status update:', status);
|
|
try {
|
|
startDatabaseOperation();
|
|
if (!pluginState.instance) {
|
|
throw new SQLiteError('Plugin not initialized', 'sqlite-status');
|
|
}
|
|
|
|
// Verify database is still open
|
|
const isOpen = await pluginState.instance.isDBOpen({ database: status.database });
|
|
if (!isOpen) {
|
|
throw new SQLiteError('Database not open', 'sqlite-status');
|
|
}
|
|
|
|
logger.info('SQLite status update processed:', {
|
|
status: status.status,
|
|
database: status.database,
|
|
timestamp: new Date(status.timestamp).toISOString()
|
|
});
|
|
|
|
return { success: true, isOpen };
|
|
} catch (error) {
|
|
logger.error('SQLite status update failed:', error);
|
|
throw error;
|
|
} finally {
|
|
endDatabaseOperation();
|
|
}
|
|
});
|
|
|
|
logger.info('SQLite IPC handlers setup complete');
|
|
}
|