|
|
@ -4,21 +4,188 @@ import { |
|
|
|
PlatformCapabilities, |
|
|
|
} from "../PlatformService"; |
|
|
|
import { logger } from "../../utils/logger"; |
|
|
|
import { QueryExecResult } from "@/interfaces/database"; |
|
|
|
import { QueryExecResult, SqlValue } from "@/interfaces/database"; |
|
|
|
import { |
|
|
|
SQLiteConnection, |
|
|
|
SQLiteDBConnection, |
|
|
|
CapacitorSQLite, |
|
|
|
Changes, |
|
|
|
} from "@capacitor-community/sqlite"; |
|
|
|
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; |
|
|
|
|
|
|
|
interface Migration { |
|
|
|
name: string; |
|
|
|
sql: string; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Platform service implementation for Electron (desktop) platform. |
|
|
|
* Note: This is a placeholder implementation with most methods currently unimplemented. |
|
|
|
* Implements the PlatformService interface but throws "Not implemented" errors for most operations. |
|
|
|
* |
|
|
|
* @remarks |
|
|
|
* This service is intended for desktop application functionality through Electron. |
|
|
|
* Future implementations should provide: |
|
|
|
* - Native file system access |
|
|
|
* - Desktop camera integration |
|
|
|
* - System-level features |
|
|
|
* Provides native desktop functionality through Electron and Capacitor plugins for: |
|
|
|
* - File system operations (TODO) |
|
|
|
* - Camera integration (TODO) |
|
|
|
* - SQLite database operations |
|
|
|
* - System-level features (TODO) |
|
|
|
*/ |
|
|
|
export class ElectronPlatformService implements PlatformService { |
|
|
|
private sqlite: SQLiteConnection; |
|
|
|
private db: SQLiteDBConnection | null = null; |
|
|
|
private dbName = "timesafari.db"; |
|
|
|
private initialized = false; |
|
|
|
|
|
|
|
constructor() { |
|
|
|
this.sqlite = new SQLiteConnection(CapacitorSQLite); |
|
|
|
} |
|
|
|
|
|
|
|
private async initializeDatabase(): Promise<void> { |
|
|
|
if (this.initialized) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// Create/Open database
|
|
|
|
this.db = await this.sqlite.createConnection( |
|
|
|
this.dbName, |
|
|
|
false, |
|
|
|
"no-encryption", |
|
|
|
1, |
|
|
|
false, |
|
|
|
); |
|
|
|
|
|
|
|
await this.db.open(); |
|
|
|
|
|
|
|
// Set journal mode to WAL for better performance
|
|
|
|
await this.db.execute("PRAGMA journal_mode=WAL;"); |
|
|
|
|
|
|
|
// Run migrations
|
|
|
|
await this.runMigrations(); |
|
|
|
|
|
|
|
this.initialized = true; |
|
|
|
logger.log("SQLite database initialized successfully"); |
|
|
|
} catch (error) { |
|
|
|
logger.error("Error initializing SQLite database:", error); |
|
|
|
throw new Error("Failed to initialize database"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private async runMigrations(): Promise<void> { |
|
|
|
if (!this.db) { |
|
|
|
throw new Error("Database not initialized"); |
|
|
|
} |
|
|
|
|
|
|
|
// Create migrations table if it doesn't exist
|
|
|
|
await this.db.execute(` |
|
|
|
CREATE TABLE IF NOT EXISTS migrations ( |
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
|
|
name TEXT NOT NULL UNIQUE, |
|
|
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
|
|
|
); |
|
|
|
`);
|
|
|
|
|
|
|
|
// Get list of executed migrations
|
|
|
|
const result = await this.db.query("SELECT name FROM migrations;"); |
|
|
|
const executedMigrations = new Set( |
|
|
|
result.values?.map((row) => row[0]) || [], |
|
|
|
); |
|
|
|
|
|
|
|
// Run pending migrations in order
|
|
|
|
const migrations: Migration[] = [ |
|
|
|
{ |
|
|
|
name: "001_initial", |
|
|
|
sql: ` |
|
|
|
CREATE TABLE IF NOT EXISTS accounts ( |
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
|
|
dateCreated TEXT NOT NULL, |
|
|
|
derivationPath TEXT, |
|
|
|
did TEXT NOT NULL, |
|
|
|
identityEncrBase64 TEXT, |
|
|
|
mnemonicEncrBase64 TEXT, |
|
|
|
passkeyCredIdHex TEXT, |
|
|
|
publicKeyHex TEXT NOT NULL |
|
|
|
); |
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did); |
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS secret ( |
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
|
|
secretBase64 TEXT NOT NULL |
|
|
|
); |
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS settings ( |
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
|
|
accountDid TEXT, |
|
|
|
activeDid TEXT, |
|
|
|
apiServer TEXT, |
|
|
|
filterFeedByNearby BOOLEAN, |
|
|
|
filterFeedByVisible BOOLEAN, |
|
|
|
finishedOnboarding BOOLEAN, |
|
|
|
firstName TEXT, |
|
|
|
hideRegisterPromptOnNewContact BOOLEAN, |
|
|
|
isRegistered BOOLEAN, |
|
|
|
lastName TEXT, |
|
|
|
lastAckedOfferToUserJwtId TEXT, |
|
|
|
lastAckedOfferToUserProjectsJwtId TEXT, |
|
|
|
lastNotifiedClaimId TEXT, |
|
|
|
lastViewedClaimId TEXT, |
|
|
|
notifyingNewActivityTime TEXT, |
|
|
|
notifyingReminderMessage TEXT, |
|
|
|
notifyingReminderTime TEXT, |
|
|
|
partnerApiServer TEXT, |
|
|
|
passkeyExpirationMinutes INTEGER, |
|
|
|
profileImageUrl TEXT, |
|
|
|
searchBoxes TEXT, |
|
|
|
showContactGivesInline BOOLEAN, |
|
|
|
showGeneralAdvanced BOOLEAN, |
|
|
|
showShortcutBvc BOOLEAN, |
|
|
|
vapid TEXT, |
|
|
|
warnIfProdServer BOOLEAN, |
|
|
|
warnIfTestServer BOOLEAN, |
|
|
|
webPushServer TEXT |
|
|
|
); |
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid); |
|
|
|
|
|
|
|
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}'); |
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS contacts ( |
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
|
|
did TEXT NOT NULL, |
|
|
|
name TEXT, |
|
|
|
contactMethods TEXT, |
|
|
|
nextPubKeyHashB64 TEXT, |
|
|
|
notes TEXT, |
|
|
|
profileImageUrl TEXT, |
|
|
|
publicKeyBase64 TEXT, |
|
|
|
seesMe BOOLEAN, |
|
|
|
registered BOOLEAN |
|
|
|
); |
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did); |
|
|
|
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name); |
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS logs ( |
|
|
|
date TEXT PRIMARY KEY, |
|
|
|
message TEXT NOT NULL |
|
|
|
); |
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS temp ( |
|
|
|
id TEXT PRIMARY KEY, |
|
|
|
blobB64 TEXT |
|
|
|
); |
|
|
|
`,
|
|
|
|
}, |
|
|
|
]; |
|
|
|
|
|
|
|
for (const migration of migrations) { |
|
|
|
if (!executedMigrations.has(migration.name)) { |
|
|
|
await this.db.execute(migration.sql); |
|
|
|
await this.db.run("INSERT INTO migrations (name) VALUES (?)", [ |
|
|
|
migration.name, |
|
|
|
]); |
|
|
|
logger.log(`Migration ${migration.name} executed successfully`); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Gets the capabilities of the Electron platform |
|
|
|
* @returns Platform capabilities object |
|
|
@ -56,6 +223,17 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
throw new Error("Not implemented"); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Writes content to a file and opens the system share dialog. |
|
|
|
* @param _fileName - Name of the file to create |
|
|
|
* @param _content - Content to write to the file |
|
|
|
* @throws Error with "Not implemented" message |
|
|
|
* @todo Implement using Electron's dialog and file system APIs |
|
|
|
*/ |
|
|
|
async writeAndShareFile(_fileName: string, _content: string): Promise<void> { |
|
|
|
throw new Error("Not implemented"); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Deletes a file from the filesystem. |
|
|
|
* @param _path - Path to the file to delete |
|
|
@ -110,13 +288,54 @@ export class ElectronPlatformService implements PlatformService { |
|
|
|
throw new Error("Not implemented"); |
|
|
|
} |
|
|
|
|
|
|
|
dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> { |
|
|
|
throw new Error("Not implemented for " + sql + " with params " + params); |
|
|
|
/** |
|
|
|
* @see PlatformService.dbQuery |
|
|
|
*/ |
|
|
|
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> { |
|
|
|
await this.initializeDatabase(); |
|
|
|
if (!this.db) { |
|
|
|
throw new Error("Database not initialized"); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const result = await this.db.query(sql, params || []); |
|
|
|
const values = result.values || []; |
|
|
|
return { |
|
|
|
columns: [], // SQLite plugin doesn't provide column names in query result
|
|
|
|
values: values as SqlValue[][], |
|
|
|
}; |
|
|
|
} catch (error) { |
|
|
|
logger.error("Error executing query:", error); |
|
|
|
throw new Error( |
|
|
|
`Database query failed: ${error instanceof Error ? error.message : String(error)}`, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
dbExec( |
|
|
|
|
|
|
|
/** |
|
|
|
* @see PlatformService.dbExec |
|
|
|
*/ |
|
|
|
async dbExec( |
|
|
|
sql: string, |
|
|
|
params?: unknown[], |
|
|
|
): Promise<{ changes: number; lastId?: number }> { |
|
|
|
throw new Error("Not implemented for " + sql + " with params " + params); |
|
|
|
await this.initializeDatabase(); |
|
|
|
if (!this.db) { |
|
|
|
throw new Error("Database not initialized"); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const result = await this.db.run(sql, params || []); |
|
|
|
const changes = result.changes as Changes; |
|
|
|
return { |
|
|
|
changes: changes?.changes || 0, |
|
|
|
lastId: changes?.lastId, |
|
|
|
}; |
|
|
|
} catch (error) { |
|
|
|
logger.error("Error executing statement:", error); |
|
|
|
throw new Error( |
|
|
|
`Database execution failed: ${error instanceof Error ? error.message : String(error)}`, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|