Browse Source

feat(sqlite): Database file creation working, connection pending

- Successfully creates database file using plugin's open() method
- Directory permissions and path handling verified working
- Plugin initialization and echo test passing
- Database file created at /home/matthew/Databases/TimeSafari/timesafariSQLite.db

Key findings:
- createConnection() returns undefined but doesn't error
- open() silently creates the database file
- Connection retrieval still needs work (getDatabaseConnectionOrThrowError fails)
- Plugin structure confirmed: both class and default export available

Next steps:
- Refine connection handling after database creation
- Add connection state verification
- Consider adding retry logic for connection retrieval

Technical details:
- Using CapacitorSQLite from @capacitor-community/sqlite/electron
- Database path: /home/matthew/Databases/TimeSafari/timesafariSQLite.db
- Directory permissions: 755 (rwxr-xr-x)
- Plugin version: 6.x (Capacitor 6+ compatible)
pull/134/head
Matthew Raymer 1 week ago
parent
commit
57191df416
  1. 3
      electron/capacitor.config.json
  2. 457
      electron/src/rt/sqlite-init.ts

3
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": {

457
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<string>();
// 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<string> => {
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<void> => {
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<void> | 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<void> {
if (sqliteInitializationPromise) {
logger.info('SQLite initialization already in progress, waiting...');
@ -168,12 +214,67 @@ export async function initializeSQLite(): Promise<void> {
// 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<void> {
}
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') {

Loading…
Cancel
Save