Browse Source

feature: fleshed out capacitor and electron database operators

pull/137/head
Matthew Raymer 2 weeks ago
parent
commit
1560ff0829
  1. 223
      src/services/platforms/CapacitorPlatformService.ts
  2. 247
      src/services/platforms/ElectronPlatformService.ts

223
src/services/platforms/CapacitorPlatformService.ts

@ -6,8 +6,20 @@ import {
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem"; import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import { Share } from "@capacitor/share"; import { Share } from "@capacitor/share";
import {
SQLiteConnection,
SQLiteDBConnection,
CapacitorSQLite,
Changes,
} from "@capacitor-community/sqlite";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database"; import { QueryExecResult, SqlValue } from "@/interfaces/database";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
interface Migration {
name: string;
sql: string;
}
/** /**
* Platform service implementation for Capacitor (mobile) platform. * Platform service implementation for Capacitor (mobile) platform.
@ -15,8 +27,168 @@ import { QueryExecResult } from "@/interfaces/database";
* - File system operations * - File system operations
* - Camera and image picker * - Camera and image picker
* - Platform-specific features * - Platform-specific features
* - SQLite database operations
*/ */
export class CapacitorPlatformService implements PlatformService { export class CapacitorPlatformService 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 Capacitor platform * Gets the capabilities of the Capacitor platform
* @returns Platform capabilities object * @returns Platform capabilities object
@ -478,13 +650,54 @@ export class CapacitorPlatformService implements PlatformService {
return Promise.resolve(); return Promise.resolve();
} }
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, sql: string,
params?: unknown[], params?: unknown[],
): Promise<{ changes: number; lastId?: number }> { ): 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)}`,
);
}
} }
} }

247
src/services/platforms/ElectronPlatformService.ts

@ -4,21 +4,188 @@ import {
PlatformCapabilities, PlatformCapabilities,
} from "../PlatformService"; } from "../PlatformService";
import { logger } from "../../utils/logger"; 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. * Platform service implementation for Electron (desktop) platform.
* Note: This is a placeholder implementation with most methods currently unimplemented. * Provides native desktop functionality through Electron and Capacitor plugins for:
* Implements the PlatformService interface but throws "Not implemented" errors for most operations. * - File system operations (TODO)
* * - Camera integration (TODO)
* @remarks * - SQLite database operations
* This service is intended for desktop application functionality through Electron. * - System-level features (TODO)
* Future implementations should provide:
* - Native file system access
* - Desktop camera integration
* - System-level features
*/ */
export class ElectronPlatformService implements PlatformService { 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 * Gets the capabilities of the Electron platform
* @returns Platform capabilities object * @returns Platform capabilities object
@ -56,6 +223,17 @@ export class ElectronPlatformService implements PlatformService {
throw new Error("Not implemented"); 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. * Deletes a file from the filesystem.
* @param _path - Path to the file to delete * @param _path - Path to the file to delete
@ -110,13 +288,54 @@ export class ElectronPlatformService implements PlatformService {
throw new Error("Not implemented"); 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, sql: string,
params?: unknown[], params?: unknown[],
): Promise<{ changes: number; lastId?: number }> { ): 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)}`,
);
}
} }
} }

Loading…
Cancel
Save