refactor(db): improve type safety in migration system
- Replace any[] with SqlValue[] type for SQL parameters in runMigrations - Update import to use QueryExecResult from interfaces/database - Add proper typing for SQL parameter values (string | number | null | Uint8Array) This change improves type safety and helps catch potential SQL parameter type mismatches at compile time, reducing the risk of runtime errors or data corruption.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import migrationService from "../services/migrationService";
|
import migrationService from "../services/migrationService";
|
||||||
import type { QueryExecResult } from "../services/migrationService";
|
import type { QueryExecResult, SqlValue } from "../interfaces/database";
|
||||||
|
|
||||||
// Each migration can include multiple SQL statements (with semicolons)
|
// Each migration can include multiple SQL statements (with semicolons)
|
||||||
const MIGRATIONS = [
|
const MIGRATIONS = [
|
||||||
@@ -96,7 +96,10 @@ export async function registerMigrations(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runMigrations(
|
export async function runMigrations(
|
||||||
sqlExec: (sql: string, params?: any[]) => Promise<Array<QueryExecResult>>,
|
sqlExec: (
|
||||||
|
sql: string,
|
||||||
|
params?: SqlValue[],
|
||||||
|
) => Promise<Array<QueryExecResult>>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await registerMigrations();
|
await registerMigrations();
|
||||||
await migrationService.runMigrations(sqlExec);
|
await migrationService.runMigrations(sqlExec);
|
||||||
|
|||||||
@@ -90,21 +90,18 @@ db.on("populate", async () => {
|
|||||||
try {
|
try {
|
||||||
await db.settings.add(DEFAULT_SETTINGS);
|
await db.settings.add(DEFAULT_SETTINGS);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
logger.error("Error populating the database with default settings:", error);
|
||||||
"Error populating the database with default settings:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to safely open the database with retries
|
// Helper function to safely open the database with retries
|
||||||
async function safeOpenDatabase(retries = 1, delay = 500): Promise<void> {
|
async function safeOpenDatabase(retries = 1, delay = 500): Promise<void> {
|
||||||
// console.log("Starting safeOpenDatabase with retries:", retries);
|
// logger.log("Starting safeOpenDatabase with retries:", retries);
|
||||||
for (let i = 0; i < retries; i++) {
|
for (let i = 0; i < retries; i++) {
|
||||||
try {
|
try {
|
||||||
// console.log(`Attempt ${i + 1}: Checking if database is open...`);
|
// logger.log(`Attempt ${i + 1}: Checking if database is open...`);
|
||||||
if (!db.isOpen()) {
|
if (!db.isOpen()) {
|
||||||
// console.log(`Attempt ${i + 1}: Database is closed, attempting to open...`);
|
// logger.log(`Attempt ${i + 1}: Database is closed, attempting to open...`);
|
||||||
|
|
||||||
// Create a promise that rejects after 5 seconds
|
// Create a promise that rejects after 5 seconds
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
@@ -113,19 +110,19 @@ async function safeOpenDatabase(retries = 1, delay = 500): Promise<void> {
|
|||||||
|
|
||||||
// Race between the open operation and the timeout
|
// Race between the open operation and the timeout
|
||||||
const openPromise = db.open();
|
const openPromise = db.open();
|
||||||
// console.log(`Attempt ${i + 1}: Waiting for db.open() promise...`);
|
// logger.log(`Attempt ${i + 1}: Waiting for db.open() promise...`);
|
||||||
await Promise.race([openPromise, timeoutPromise]);
|
await Promise.race([openPromise, timeoutPromise]);
|
||||||
|
|
||||||
// If we get here, the open succeeded
|
// If we get here, the open succeeded
|
||||||
// console.log(`Attempt ${i + 1}: Database opened successfully`);
|
// logger.log(`Attempt ${i + 1}: Database opened successfully`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// console.log(`Attempt ${i + 1}: Database was already open`);
|
// logger.log(`Attempt ${i + 1}: Database was already open`);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Attempt ${i + 1}: Database open failed:`, error);
|
logger.error(`Attempt ${i + 1}: Database open failed:`, error);
|
||||||
if (i < retries - 1) {
|
if (i < retries - 1) {
|
||||||
console.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`);
|
logger.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -142,12 +139,12 @@ export async function updateDefaultSettings(
|
|||||||
delete settingsChanges.id;
|
delete settingsChanges.id;
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
// console.log("Database state before open:", db.isOpen() ? "open" : "closed");
|
// logger.log("Database state before open:", db.isOpen() ? "open" : "closed");
|
||||||
// console.log("Database name:", db.name);
|
// logger.log("Database name:", db.name);
|
||||||
// console.log("Database version:", db.verno);
|
// logger.log("Database version:", db.verno);
|
||||||
await safeOpenDatabase();
|
await safeOpenDatabase();
|
||||||
} catch (openError: unknown) {
|
} catch (openError: unknown) {
|
||||||
console.error("Failed to open database:", openError, String(openError));
|
logger.error("Failed to open database:", openError, String(openError));
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The database connection failed. We recommend you try again or restart the app.`,
|
`The database connection failed. We recommend you try again or restart the app.`,
|
||||||
);
|
);
|
||||||
@@ -158,7 +155,7 @@ export async function updateDefaultSettings(
|
|||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating default settings:", error);
|
logger.error("Error updating default settings:", error);
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
throw error; // Re-throw if it's already an Error with a message
|
throw error; // Re-throw if it's already an Error with a message
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
/**
|
|
||||||
* SQLite Database Initialization
|
|
||||||
*
|
|
||||||
* This module handles database initialization, including:
|
|
||||||
* - Database connection management
|
|
||||||
* - Schema creation and migration
|
|
||||||
* - Connection pooling and lifecycle
|
|
||||||
* - Error handling and recovery
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Database, SQLite3 } from '@wa-sqlite/sql.js';
|
|
||||||
import { DATABASE_SCHEMA, SQLiteTable } from './types';
|
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Database Connection Management
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface DatabaseConnection {
|
|
||||||
db: Database;
|
|
||||||
sqlite3: SQLite3;
|
|
||||||
isOpen: boolean;
|
|
||||||
lastUsed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
let connection: DatabaseConnection | null = null;
|
|
||||||
const CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the SQLite database connection
|
|
||||||
*/
|
|
||||||
export async function initDatabase(): Promise<DatabaseConnection> {
|
|
||||||
if (connection?.isOpen) {
|
|
||||||
connection.lastUsed = Date.now();
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sqlite3 = await import('@wa-sqlite/sql.js');
|
|
||||||
const db = await sqlite3.open(':memory:'); // TODO: Configure storage location
|
|
||||||
|
|
||||||
// Enable foreign keys
|
|
||||||
await db.exec('PRAGMA foreign_keys = ON;');
|
|
||||||
|
|
||||||
// Configure for better performance
|
|
||||||
await db.exec(`
|
|
||||||
PRAGMA journal_mode = WAL;
|
|
||||||
PRAGMA synchronous = NORMAL;
|
|
||||||
PRAGMA cache_size = -2000; -- Use 2MB of cache
|
|
||||||
`);
|
|
||||||
|
|
||||||
connection = {
|
|
||||||
db,
|
|
||||||
sqlite3,
|
|
||||||
isOpen: true,
|
|
||||||
lastUsed: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start connection cleanup interval
|
|
||||||
startConnectionCleanup();
|
|
||||||
|
|
||||||
return connection;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Database initialization failed:', error);
|
|
||||||
throw new Error('Failed to initialize database');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the database connection
|
|
||||||
*/
|
|
||||||
export async function closeDatabase(): Promise<void> {
|
|
||||||
if (!connection?.isOpen) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await connection.db.close();
|
|
||||||
connection.isOpen = false;
|
|
||||||
connection = null;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Database close failed:', error);
|
|
||||||
throw new Error('Failed to close database');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup inactive connections
|
|
||||||
*/
|
|
||||||
function startConnectionCleanup(): void {
|
|
||||||
setInterval(() => {
|
|
||||||
if (connection && Date.now() - connection.lastUsed > CONNECTION_TIMEOUT) {
|
|
||||||
closeDatabase().catch(error => {
|
|
||||||
logger.error('[SQLite] Connection cleanup failed:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 60000); // Check every minute
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Schema Management
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the database schema
|
|
||||||
*/
|
|
||||||
export async function createSchema(): Promise<void> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.transaction(async () => {
|
|
||||||
for (const table of DATABASE_SCHEMA) {
|
|
||||||
await createTable(db, table);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Schema creation failed:', error);
|
|
||||||
throw new Error('Failed to create database schema');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a single table
|
|
||||||
*/
|
|
||||||
async function createTable(db: Database, table: SQLiteTable): Promise<void> {
|
|
||||||
const columnDefs = table.columns.map(col => {
|
|
||||||
const constraints = [
|
|
||||||
col.primaryKey ? 'PRIMARY KEY' : '',
|
|
||||||
col.unique ? 'UNIQUE' : '',
|
|
||||||
!col.nullable ? 'NOT NULL' : '',
|
|
||||||
col.references ? `REFERENCES ${col.references.table}(${col.references.column})` : '',
|
|
||||||
col.default !== undefined ? `DEFAULT ${formatDefaultValue(col.default)}` : ''
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return `${col.name} ${col.type} ${constraints}`.trim();
|
|
||||||
});
|
|
||||||
|
|
||||||
const createTableSQL = `
|
|
||||||
CREATE TABLE IF NOT EXISTS ${table.name} (
|
|
||||||
${columnDefs.join(',\n ')}
|
|
||||||
);
|
|
||||||
`;
|
|
||||||
|
|
||||||
await db.exec(createTableSQL);
|
|
||||||
|
|
||||||
// Create indexes
|
|
||||||
if (table.indexes) {
|
|
||||||
for (const index of table.indexes) {
|
|
||||||
const createIndexSQL = `
|
|
||||||
CREATE INDEX IF NOT EXISTS ${index.name}
|
|
||||||
ON ${table.name} (${index.columns.join(', ')})
|
|
||||||
${index.unique ? 'UNIQUE' : ''};
|
|
||||||
`;
|
|
||||||
await db.exec(createIndexSQL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format default value for SQL
|
|
||||||
*/
|
|
||||||
function formatDefaultValue(value: unknown): string {
|
|
||||||
if (value === null) return 'NULL';
|
|
||||||
if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`;
|
|
||||||
if (typeof value === 'number') return value.toString();
|
|
||||||
if (typeof value === 'boolean') return value ? '1' : '0';
|
|
||||||
throw new Error(`Unsupported default value type: ${typeof value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Database Health Checks
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check database health
|
|
||||||
*/
|
|
||||||
export async function checkDatabaseHealth(): Promise<{
|
|
||||||
isHealthy: boolean;
|
|
||||||
tables: string[];
|
|
||||||
error?: string;
|
|
||||||
}> {
|
|
||||||
try {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
// Check if we can query the database
|
|
||||||
const tables = await db.selectAll<{ name: string }>(`
|
|
||||||
SELECT name FROM sqlite_master
|
|
||||||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
||||||
`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isHealthy: true,
|
|
||||||
tables: tables.map(t => t.name)
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Health check failed:', error);
|
|
||||||
return {
|
|
||||||
isHealthy: false,
|
|
||||||
tables: [],
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify database integrity
|
|
||||||
*/
|
|
||||||
export async function verifyDatabaseIntegrity(): Promise<{
|
|
||||||
isIntegrityOk: boolean;
|
|
||||||
errors: string[];
|
|
||||||
}> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Run integrity check
|
|
||||||
const result = await db.selectAll<{ integrity_check: string }>('PRAGMA integrity_check;');
|
|
||||||
|
|
||||||
if (result[0]?.integrity_check !== 'ok') {
|
|
||||||
errors.push('Database integrity check failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check foreign key constraints
|
|
||||||
const fkResult = await db.selectAll<{ table: string; rowid: number; parent: string; fkid: number }>(`
|
|
||||||
PRAGMA foreign_key_check;
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (fkResult.length > 0) {
|
|
||||||
errors.push('Foreign key constraint violations found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isIntegrityOk: errors.length === 0,
|
|
||||||
errors
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Integrity check failed:', error);
|
|
||||||
return {
|
|
||||||
isIntegrityOk: false,
|
|
||||||
errors: [error instanceof Error ? error.message : 'Unknown error']
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Database Backup and Recovery
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a database backup
|
|
||||||
*/
|
|
||||||
export async function createBackup(): Promise<Uint8Array> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Export the database to a binary array
|
|
||||||
return await db.export();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Backup creation failed:', error);
|
|
||||||
throw new Error('Failed to create database backup');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore database from backup
|
|
||||||
*/
|
|
||||||
export async function restoreFromBackup(backup: Uint8Array): Promise<void> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Close current connection
|
|
||||||
await closeDatabase();
|
|
||||||
|
|
||||||
// Create new connection and import backup
|
|
||||||
const sqlite3 = await import('@wa-sqlite/sql.js');
|
|
||||||
const newDb = await sqlite3.open(backup);
|
|
||||||
|
|
||||||
// Verify integrity
|
|
||||||
const { isIntegrityOk, errors } = await verifyDatabaseIntegrity();
|
|
||||||
if (!isIntegrityOk) {
|
|
||||||
throw new Error(`Backup integrity check failed: ${errors.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace current connection
|
|
||||||
connection = {
|
|
||||||
db: newDb,
|
|
||||||
sqlite3,
|
|
||||||
isOpen: true,
|
|
||||||
lastUsed: Date.now()
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Backup restoration failed:', error);
|
|
||||||
throw new Error('Failed to restore database from backup');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
/**
|
|
||||||
* SQLite Migration Utilities
|
|
||||||
*
|
|
||||||
* This module handles the migration of data from Dexie to SQLite,
|
|
||||||
* including data transformation, validation, and rollback capabilities.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Database } from '@wa-sqlite/sql.js';
|
|
||||||
import { initDatabase, createSchema, createBackup } from './init';
|
|
||||||
import {
|
|
||||||
MigrationData,
|
|
||||||
MigrationResult,
|
|
||||||
SQLiteAccount,
|
|
||||||
SQLiteContact,
|
|
||||||
SQLiteContactMethod,
|
|
||||||
SQLiteSettings,
|
|
||||||
SQLiteLog,
|
|
||||||
SQLiteSecret,
|
|
||||||
isSQLiteAccount,
|
|
||||||
isSQLiteContact,
|
|
||||||
isSQLiteSettings
|
|
||||||
} from './types';
|
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Migration Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
interface MigrationContext {
|
|
||||||
db: Database;
|
|
||||||
startTime: number;
|
|
||||||
stats: MigrationResult['stats'];
|
|
||||||
errors: Error[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Migration Functions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate data from Dexie to SQLite
|
|
||||||
*/
|
|
||||||
export async function migrateFromDexie(data: MigrationData): Promise<MigrationResult> {
|
|
||||||
const startTime = Date.now();
|
|
||||||
const context: MigrationContext = {
|
|
||||||
db: (await initDatabase()).db,
|
|
||||||
startTime,
|
|
||||||
stats: {
|
|
||||||
accounts: 0,
|
|
||||||
contacts: 0,
|
|
||||||
contactMethods: 0,
|
|
||||||
settings: 0,
|
|
||||||
logs: 0,
|
|
||||||
secrets: 0
|
|
||||||
},
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create backup before migration
|
|
||||||
const backup = await createBackup();
|
|
||||||
|
|
||||||
// Create schema if needed
|
|
||||||
await createSchema();
|
|
||||||
|
|
||||||
// Perform migration in a transaction
|
|
||||||
await context.db.transaction(async () => {
|
|
||||||
// Migrate in order of dependencies
|
|
||||||
await migrateAccounts(context, data.accounts);
|
|
||||||
await migrateContacts(context, data.contacts);
|
|
||||||
await migrateContactMethods(context, data.contactMethods);
|
|
||||||
await migrateSettings(context, data.settings);
|
|
||||||
await migrateLogs(context, data.logs);
|
|
||||||
await migrateSecrets(context, data.secrets);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify migration
|
|
||||||
const verificationResult = await verifyMigration(context, data);
|
|
||||||
if (!verificationResult.success) {
|
|
||||||
throw new Error(`Migration verification failed: ${verificationResult.error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
stats: context.stats,
|
|
||||||
duration: Date.now() - startTime
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Migration failed:', error);
|
|
||||||
|
|
||||||
// Attempt rollback
|
|
||||||
try {
|
|
||||||
await rollbackMigration(backup);
|
|
||||||
} catch (rollbackError) {
|
|
||||||
logger.error('[SQLite] Rollback failed:', rollbackError);
|
|
||||||
context.errors.push(new Error('Migration and rollback failed'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error : new Error('Unknown migration error'),
|
|
||||||
stats: context.stats,
|
|
||||||
duration: Date.now() - startTime
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Migration Helpers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate accounts
|
|
||||||
*/
|
|
||||||
async function migrateAccounts(context: MigrationContext, accounts: SQLiteAccount[]): Promise<void> {
|
|
||||||
for (const account of accounts) {
|
|
||||||
try {
|
|
||||||
if (!isSQLiteAccount(account)) {
|
|
||||||
throw new Error(`Invalid account data: ${JSON.stringify(account)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.db.exec(`
|
|
||||||
INSERT INTO accounts (
|
|
||||||
did, public_key_hex, created_at, updated_at,
|
|
||||||
identity_json, mnemonic_encrypted, passkey_cred_id_hex, derivation_path
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
account.did,
|
|
||||||
account.public_key_hex,
|
|
||||||
account.created_at,
|
|
||||||
account.updated_at,
|
|
||||||
account.identity_json || null,
|
|
||||||
account.mnemonic_encrypted || null,
|
|
||||||
account.passkey_cred_id_hex || null,
|
|
||||||
account.derivation_path || null
|
|
||||||
]);
|
|
||||||
|
|
||||||
context.stats.accounts++;
|
|
||||||
} catch (error) {
|
|
||||||
context.errors.push(new Error(`Failed to migrate account ${account.did}: ${error}`));
|
|
||||||
throw error; // Re-throw to trigger transaction rollback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate contacts
|
|
||||||
*/
|
|
||||||
async function migrateContacts(context: MigrationContext, contacts: SQLiteContact[]): Promise<void> {
|
|
||||||
for (const contact of contacts) {
|
|
||||||
try {
|
|
||||||
if (!isSQLiteContact(contact)) {
|
|
||||||
throw new Error(`Invalid contact data: ${JSON.stringify(contact)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.db.exec(`
|
|
||||||
INSERT INTO contacts (
|
|
||||||
id, did, name, notes, profile_image_url,
|
|
||||||
public_key_base64, next_pub_key_hash_b64,
|
|
||||||
sees_me, registered, created_at, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
contact.id,
|
|
||||||
contact.did,
|
|
||||||
contact.name || null,
|
|
||||||
contact.notes || null,
|
|
||||||
contact.profile_image_url || null,
|
|
||||||
contact.public_key_base64 || null,
|
|
||||||
contact.next_pub_key_hash_b64 || null,
|
|
||||||
contact.sees_me ? 1 : 0,
|
|
||||||
contact.registered ? 1 : 0,
|
|
||||||
contact.created_at,
|
|
||||||
contact.updated_at
|
|
||||||
]);
|
|
||||||
|
|
||||||
context.stats.contacts++;
|
|
||||||
} catch (error) {
|
|
||||||
context.errors.push(new Error(`Failed to migrate contact ${contact.id}: ${error}`));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate contact methods
|
|
||||||
*/
|
|
||||||
async function migrateContactMethods(
|
|
||||||
context: MigrationContext,
|
|
||||||
methods: SQLiteContactMethod[]
|
|
||||||
): Promise<void> {
|
|
||||||
for (const method of methods) {
|
|
||||||
try {
|
|
||||||
await context.db.exec(`
|
|
||||||
INSERT INTO contact_methods (
|
|
||||||
id, contact_id, label, type, value,
|
|
||||||
created_at, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
method.id,
|
|
||||||
method.contact_id,
|
|
||||||
method.label,
|
|
||||||
method.type,
|
|
||||||
method.value,
|
|
||||||
method.created_at,
|
|
||||||
method.updated_at
|
|
||||||
]);
|
|
||||||
|
|
||||||
context.stats.contactMethods++;
|
|
||||||
} catch (error) {
|
|
||||||
context.errors.push(new Error(`Failed to migrate contact method ${method.id}: ${error}`));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate settings
|
|
||||||
*/
|
|
||||||
async function migrateSettings(context: MigrationContext, settings: SQLiteSettings[]): Promise<void> {
|
|
||||||
for (const setting of settings) {
|
|
||||||
try {
|
|
||||||
if (!isSQLiteSettings(setting)) {
|
|
||||||
throw new Error(`Invalid settings data: ${JSON.stringify(setting)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.db.exec(`
|
|
||||||
INSERT INTO settings (
|
|
||||||
key, account_did, value_json, created_at, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
setting.key,
|
|
||||||
setting.account_did || null,
|
|
||||||
setting.value_json,
|
|
||||||
setting.created_at,
|
|
||||||
setting.updated_at
|
|
||||||
]);
|
|
||||||
|
|
||||||
context.stats.settings++;
|
|
||||||
} catch (error) {
|
|
||||||
context.errors.push(new Error(`Failed to migrate setting ${setting.key}: ${error}`));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate logs
|
|
||||||
*/
|
|
||||||
async function migrateLogs(context: MigrationContext, logs: SQLiteLog[]): Promise<void> {
|
|
||||||
for (const log of logs) {
|
|
||||||
try {
|
|
||||||
await context.db.exec(`
|
|
||||||
INSERT INTO logs (
|
|
||||||
id, level, message, metadata_json, created_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
log.id,
|
|
||||||
log.level,
|
|
||||||
log.message,
|
|
||||||
log.metadata_json || null,
|
|
||||||
log.created_at
|
|
||||||
]);
|
|
||||||
|
|
||||||
context.stats.logs++;
|
|
||||||
} catch (error) {
|
|
||||||
context.errors.push(new Error(`Failed to migrate log ${log.id}: ${error}`));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate secrets
|
|
||||||
*/
|
|
||||||
async function migrateSecrets(context: MigrationContext, secrets: SQLiteSecret[]): Promise<void> {
|
|
||||||
for (const secret of secrets) {
|
|
||||||
try {
|
|
||||||
await context.db.exec(`
|
|
||||||
INSERT INTO secrets (
|
|
||||||
key, value_encrypted, created_at, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
secret.key,
|
|
||||||
secret.value_encrypted,
|
|
||||||
secret.created_at,
|
|
||||||
secret.updated_at
|
|
||||||
]);
|
|
||||||
|
|
||||||
context.stats.secrets++;
|
|
||||||
} catch (error) {
|
|
||||||
context.errors.push(new Error(`Failed to migrate secret ${secret.key}: ${error}`));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Verification and Rollback
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify migration success
|
|
||||||
*/
|
|
||||||
async function verifyMigration(
|
|
||||||
context: MigrationContext,
|
|
||||||
data: MigrationData
|
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
|
||||||
try {
|
|
||||||
// Verify counts
|
|
||||||
const counts = await context.db.selectAll<{ table: string; count: number }>(`
|
|
||||||
SELECT 'accounts' as table, COUNT(*) as count FROM accounts
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'contacts', COUNT(*) FROM contacts
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'contact_methods', COUNT(*) FROM contact_methods
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'settings', COUNT(*) FROM settings
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'logs', COUNT(*) FROM logs
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'secrets', COUNT(*) FROM secrets
|
|
||||||
`);
|
|
||||||
|
|
||||||
const countMap = new Map(counts.map(c => [c.table, c.count]));
|
|
||||||
|
|
||||||
if (countMap.get('accounts') !== data.accounts.length) {
|
|
||||||
return { success: false, error: 'Account count mismatch' };
|
|
||||||
}
|
|
||||||
if (countMap.get('contacts') !== data.contacts.length) {
|
|
||||||
return { success: false, error: 'Contact count mismatch' };
|
|
||||||
}
|
|
||||||
if (countMap.get('contact_methods') !== data.contactMethods.length) {
|
|
||||||
return { success: false, error: 'Contact method count mismatch' };
|
|
||||||
}
|
|
||||||
if (countMap.get('settings') !== data.settings.length) {
|
|
||||||
return { success: false, error: 'Settings count mismatch' };
|
|
||||||
}
|
|
||||||
if (countMap.get('logs') !== data.logs.length) {
|
|
||||||
return { success: false, error: 'Log count mismatch' };
|
|
||||||
}
|
|
||||||
if (countMap.get('secrets') !== data.secrets.length) {
|
|
||||||
return { success: false, error: 'Secret count mismatch' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown verification error'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rollback migration
|
|
||||||
*/
|
|
||||||
async function rollbackMigration(backup: Uint8Array): Promise<void> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Close current connection
|
|
||||||
await db.close();
|
|
||||||
|
|
||||||
// Restore from backup
|
|
||||||
const sqlite3 = await import('@wa-sqlite/sql.js');
|
|
||||||
await sqlite3.open(backup);
|
|
||||||
|
|
||||||
logger.info('[SQLite] Migration rollback successful');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Migration rollback failed:', error);
|
|
||||||
throw new Error('Failed to rollback migration');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
/**
|
|
||||||
* SQLite Database Operations
|
|
||||||
*
|
|
||||||
* This module provides utility functions for common database operations,
|
|
||||||
* including CRUD operations, queries, and transactions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Database } from '@wa-sqlite/sql.js';
|
|
||||||
import { initDatabase } from './init';
|
|
||||||
import {
|
|
||||||
SQLiteAccount,
|
|
||||||
SQLiteContact,
|
|
||||||
SQLiteContactMethod,
|
|
||||||
SQLiteSettings,
|
|
||||||
SQLiteLog,
|
|
||||||
SQLiteSecret,
|
|
||||||
isSQLiteAccount,
|
|
||||||
isSQLiteContact,
|
|
||||||
isSQLiteSettings
|
|
||||||
} from './types';
|
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Transaction Helpers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a function within a transaction
|
|
||||||
*/
|
|
||||||
export async function withTransaction<T>(
|
|
||||||
operation: (db: Database) => Promise<T>
|
|
||||||
): Promise<T> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await db.transaction(operation);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[SQLite] Transaction failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a function with retries
|
|
||||||
*/
|
|
||||||
export async function withRetry<T>(
|
|
||||||
operation: () => Promise<T>,
|
|
||||||
maxRetries = 3,
|
|
||||||
delay = 1000
|
|
||||||
): Promise<T> {
|
|
||||||
let lastError: Error | undefined;
|
|
||||||
|
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
|
||||||
try {
|
|
||||||
return await operation();
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error instanceof Error ? error : new Error(String(error));
|
|
||||||
|
|
||||||
if (i < maxRetries - 1) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw lastError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Account Operations
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get account by DID
|
|
||||||
*/
|
|
||||||
export async function getAccountByDid(did: string): Promise<SQLiteAccount | null> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
const accounts = await db.selectAll<SQLiteAccount>(
|
|
||||||
'SELECT * FROM accounts WHERE did = ?',
|
|
||||||
[did]
|
|
||||||
);
|
|
||||||
|
|
||||||
return accounts[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all accounts
|
|
||||||
*/
|
|
||||||
export async function getAllAccounts(): Promise<SQLiteAccount[]> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
return db.selectAll<SQLiteAccount>(
|
|
||||||
'SELECT * FROM accounts ORDER BY created_at DESC'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create or update account
|
|
||||||
*/
|
|
||||||
export async function upsertAccount(account: SQLiteAccount): Promise<void> {
|
|
||||||
if (!isSQLiteAccount(account)) {
|
|
||||||
throw new Error('Invalid account data');
|
|
||||||
}
|
|
||||||
|
|
||||||
await withTransaction(async (db) => {
|
|
||||||
const existing = await db.selectOne<{ did: string }>(
|
|
||||||
'SELECT did FROM accounts WHERE did = ?',
|
|
||||||
[account.did]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
await db.exec(`
|
|
||||||
UPDATE accounts SET
|
|
||||||
public_key_hex = ?,
|
|
||||||
updated_at = ?,
|
|
||||||
identity_json = ?,
|
|
||||||
mnemonic_encrypted = ?,
|
|
||||||
passkey_cred_id_hex = ?,
|
|
||||||
derivation_path = ?
|
|
||||||
WHERE did = ?
|
|
||||||
`, [
|
|
||||||
account.public_key_hex,
|
|
||||||
Date.now(),
|
|
||||||
account.identity_json || null,
|
|
||||||
account.mnemonic_encrypted || null,
|
|
||||||
account.passkey_cred_id_hex || null,
|
|
||||||
account.derivation_path || null,
|
|
||||||
account.did
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
await db.exec(`
|
|
||||||
INSERT INTO accounts (
|
|
||||||
did, public_key_hex, created_at, updated_at,
|
|
||||||
identity_json, mnemonic_encrypted, passkey_cred_id_hex, derivation_path
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
account.did,
|
|
||||||
account.public_key_hex,
|
|
||||||
account.created_at,
|
|
||||||
account.updated_at,
|
|
||||||
account.identity_json || null,
|
|
||||||
account.mnemonic_encrypted || null,
|
|
||||||
account.passkey_cred_id_hex || null,
|
|
||||||
account.derivation_path || null
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Contact Operations
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get contact by ID
|
|
||||||
*/
|
|
||||||
export async function getContactById(id: string): Promise<SQLiteContact | null> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
const contacts = await db.selectAll<SQLiteContact>(
|
|
||||||
'SELECT * FROM contacts WHERE id = ?',
|
|
||||||
[id]
|
|
||||||
);
|
|
||||||
|
|
||||||
return contacts[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get contacts by account DID
|
|
||||||
*/
|
|
||||||
export async function getContactsByAccountDid(did: string): Promise<SQLiteContact[]> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
return db.selectAll<SQLiteContact>(
|
|
||||||
'SELECT * FROM contacts WHERE did = ? ORDER BY created_at DESC',
|
|
||||||
[did]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get contact methods for a contact
|
|
||||||
*/
|
|
||||||
export async function getContactMethods(contactId: string): Promise<SQLiteContactMethod[]> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
return db.selectAll<SQLiteContactMethod>(
|
|
||||||
'SELECT * FROM contact_methods WHERE contact_id = ? ORDER BY created_at DESC',
|
|
||||||
[contactId]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create or update contact with methods
|
|
||||||
*/
|
|
||||||
export async function upsertContact(
|
|
||||||
contact: SQLiteContact,
|
|
||||||
methods: SQLiteContactMethod[] = []
|
|
||||||
): Promise<void> {
|
|
||||||
if (!isSQLiteContact(contact)) {
|
|
||||||
throw new Error('Invalid contact data');
|
|
||||||
}
|
|
||||||
|
|
||||||
await withTransaction(async (db) => {
|
|
||||||
const existing = await db.selectOne<{ id: string }>(
|
|
||||||
'SELECT id FROM contacts WHERE id = ?',
|
|
||||||
[contact.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
await db.exec(`
|
|
||||||
UPDATE contacts SET
|
|
||||||
did = ?,
|
|
||||||
name = ?,
|
|
||||||
notes = ?,
|
|
||||||
profile_image_url = ?,
|
|
||||||
public_key_base64 = ?,
|
|
||||||
next_pub_key_hash_b64 = ?,
|
|
||||||
sees_me = ?,
|
|
||||||
registered = ?,
|
|
||||||
updated_at = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`, [
|
|
||||||
contact.did,
|
|
||||||
contact.name || null,
|
|
||||||
contact.notes || null,
|
|
||||||
contact.profile_image_url || null,
|
|
||||||
contact.public_key_base64 || null,
|
|
||||||
contact.next_pub_key_hash_b64 || null,
|
|
||||||
contact.sees_me ? 1 : 0,
|
|
||||||
contact.registered ? 1 : 0,
|
|
||||||
Date.now(),
|
|
||||||
contact.id
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
await db.exec(`
|
|
||||||
INSERT INTO contacts (
|
|
||||||
id, did, name, notes, profile_image_url,
|
|
||||||
public_key_base64, next_pub_key_hash_b64,
|
|
||||||
sees_me, registered, created_at, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
contact.id,
|
|
||||||
contact.did,
|
|
||||||
contact.name || null,
|
|
||||||
contact.notes || null,
|
|
||||||
contact.profile_image_url || null,
|
|
||||||
contact.public_key_base64 || null,
|
|
||||||
contact.next_pub_key_hash_b64 || null,
|
|
||||||
contact.sees_me ? 1 : 0,
|
|
||||||
contact.registered ? 1 : 0,
|
|
||||||
contact.created_at,
|
|
||||||
contact.updated_at
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update contact methods
|
|
||||||
if (methods.length > 0) {
|
|
||||||
// Delete existing methods
|
|
||||||
await db.exec(
|
|
||||||
'DELETE FROM contact_methods WHERE contact_id = ?',
|
|
||||||
[contact.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Insert new methods
|
|
||||||
for (const method of methods) {
|
|
||||||
await db.exec(`
|
|
||||||
INSERT INTO contact_methods (
|
|
||||||
id, contact_id, label, type, value,
|
|
||||||
created_at, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
method.id,
|
|
||||||
contact.id,
|
|
||||||
method.label,
|
|
||||||
method.type,
|
|
||||||
method.value,
|
|
||||||
method.created_at,
|
|
||||||
method.updated_at
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Settings Operations
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get setting by key
|
|
||||||
*/
|
|
||||||
export async function getSetting(key: string): Promise<SQLiteSettings | null> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
const settings = await db.selectAll<SQLiteSettings>(
|
|
||||||
'SELECT * FROM settings WHERE key = ?',
|
|
||||||
[key]
|
|
||||||
);
|
|
||||||
|
|
||||||
return settings[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get settings by account DID
|
|
||||||
*/
|
|
||||||
export async function getSettingsByAccountDid(did: string): Promise<SQLiteSettings[]> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
return db.selectAll<SQLiteSettings>(
|
|
||||||
'SELECT * FROM settings WHERE account_did = ? ORDER BY updated_at DESC',
|
|
||||||
[did]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set setting value
|
|
||||||
*/
|
|
||||||
export async function setSetting(setting: SQLiteSettings): Promise<void> {
|
|
||||||
if (!isSQLiteSettings(setting)) {
|
|
||||||
throw new Error('Invalid settings data');
|
|
||||||
}
|
|
||||||
|
|
||||||
await withTransaction(async (db) => {
|
|
||||||
const existing = await db.selectOne<{ key: string }>(
|
|
||||||
'SELECT key FROM settings WHERE key = ?',
|
|
||||||
[setting.key]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
await db.exec(`
|
|
||||||
UPDATE settings SET
|
|
||||||
account_did = ?,
|
|
||||||
value_json = ?,
|
|
||||||
updated_at = ?
|
|
||||||
WHERE key = ?
|
|
||||||
`, [
|
|
||||||
setting.account_did || null,
|
|
||||||
setting.value_json,
|
|
||||||
Date.now(),
|
|
||||||
setting.key
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
await db.exec(`
|
|
||||||
INSERT INTO settings (
|
|
||||||
key, account_did, value_json, created_at, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
setting.key,
|
|
||||||
setting.account_did || null,
|
|
||||||
setting.value_json,
|
|
||||||
setting.created_at,
|
|
||||||
setting.updated_at
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Log Operations
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add log entry
|
|
||||||
*/
|
|
||||||
export async function addLog(log: SQLiteLog): Promise<void> {
|
|
||||||
await withTransaction(async (db) => {
|
|
||||||
await db.exec(`
|
|
||||||
INSERT INTO logs (
|
|
||||||
id, level, message, metadata_json, created_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
log.id,
|
|
||||||
log.level,
|
|
||||||
log.message,
|
|
||||||
log.metadata_json || null,
|
|
||||||
log.created_at
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get logs by level
|
|
||||||
*/
|
|
||||||
export async function getLogsByLevel(
|
|
||||||
level: string,
|
|
||||||
limit = 100,
|
|
||||||
offset = 0
|
|
||||||
): Promise<SQLiteLog[]> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
return db.selectAll<SQLiteLog>(
|
|
||||||
'SELECT * FROM logs WHERE level = ? ORDER BY created_at DESC LIMIT ? OFFSET ?',
|
|
||||||
[level, limit, offset]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Secret Operations
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get secret by key
|
|
||||||
*/
|
|
||||||
export async function getSecret(key: string): Promise<SQLiteSecret | null> {
|
|
||||||
const { db } = await initDatabase();
|
|
||||||
|
|
||||||
const secrets = await db.selectAll<SQLiteSecret>(
|
|
||||||
'SELECT * FROM secrets WHERE key = ?',
|
|
||||||
[key]
|
|
||||||
);
|
|
||||||
|
|
||||||
return secrets[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set secret value
|
|
||||||
*/
|
|
||||||
export async function setSecret(secret: SQLiteSecret): Promise<void> {
|
|
||||||
await withTransaction(async (db) => {
|
|
||||||
const existing = await db.selectOne<{ key: string }>(
|
|
||||||
'SELECT key FROM secrets WHERE key = ?',
|
|
||||||
[secret.key]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
await db.exec(`
|
|
||||||
UPDATE secrets SET
|
|
||||||
value_encrypted = ?,
|
|
||||||
updated_at = ?
|
|
||||||
WHERE key = ?
|
|
||||||
`, [
|
|
||||||
secret.value_encrypted,
|
|
||||||
Date.now(),
|
|
||||||
secret.key
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
await db.exec(`
|
|
||||||
INSERT INTO secrets (
|
|
||||||
key, value_encrypted, created_at, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
secret.key,
|
|
||||||
secret.value_encrypted,
|
|
||||||
secret.created_at,
|
|
||||||
secret.updated_at
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
/**
|
|
||||||
* SQLite Type Definitions
|
|
||||||
*
|
|
||||||
* This file defines the type system for the SQLite implementation,
|
|
||||||
* mapping from the existing Dexie types to SQLite-compatible types.
|
|
||||||
* It includes both the database schema types and the runtime types.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SQLiteCompatibleType } from '@jlongster/sql.js';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Base Types and Utilities
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite column type mapping
|
|
||||||
*/
|
|
||||||
export type SQLiteColumnType =
|
|
||||||
| 'INTEGER' // For numbers, booleans, dates
|
|
||||||
| 'TEXT' // For strings, JSON
|
|
||||||
| 'BLOB' // For binary data
|
|
||||||
| 'REAL' // For floating point numbers
|
|
||||||
| 'NULL'; // For null values
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite column definition
|
|
||||||
*/
|
|
||||||
export interface SQLiteColumn {
|
|
||||||
name: string;
|
|
||||||
type: SQLiteColumnType;
|
|
||||||
nullable?: boolean;
|
|
||||||
primaryKey?: boolean;
|
|
||||||
unique?: boolean;
|
|
||||||
references?: {
|
|
||||||
table: string;
|
|
||||||
column: string;
|
|
||||||
};
|
|
||||||
default?: SQLiteCompatibleType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite table definition
|
|
||||||
*/
|
|
||||||
export interface SQLiteTable {
|
|
||||||
name: string;
|
|
||||||
columns: SQLiteColumn[];
|
|
||||||
indexes?: Array<{
|
|
||||||
name: string;
|
|
||||||
columns: string[];
|
|
||||||
unique?: boolean;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Account Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite-compatible Account type
|
|
||||||
* Maps from the Dexie Account type
|
|
||||||
*/
|
|
||||||
export interface SQLiteAccount {
|
|
||||||
did: string; // TEXT PRIMARY KEY
|
|
||||||
public_key_hex: string; // TEXT NOT NULL
|
|
||||||
created_at: number; // INTEGER NOT NULL
|
|
||||||
updated_at: number; // INTEGER NOT NULL
|
|
||||||
identity_json?: string; // TEXT (encrypted JSON)
|
|
||||||
mnemonic_encrypted?: string; // TEXT (encrypted)
|
|
||||||
passkey_cred_id_hex?: string; // TEXT
|
|
||||||
derivation_path?: string; // TEXT
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ACCOUNTS_TABLE: SQLiteTable = {
|
|
||||||
name: 'accounts',
|
|
||||||
columns: [
|
|
||||||
{ name: 'did', type: 'TEXT', primaryKey: true },
|
|
||||||
{ name: 'public_key_hex', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'created_at', type: 'INTEGER', nullable: false },
|
|
||||||
{ name: 'updated_at', type: 'INTEGER', nullable: false },
|
|
||||||
{ name: 'identity_json', type: 'TEXT' },
|
|
||||||
{ name: 'mnemonic_encrypted', type: 'TEXT' },
|
|
||||||
{ name: 'passkey_cred_id_hex', type: 'TEXT' },
|
|
||||||
{ name: 'derivation_path', type: 'TEXT' }
|
|
||||||
],
|
|
||||||
indexes: [
|
|
||||||
{ name: 'idx_accounts_created_at', columns: ['created_at'] },
|
|
||||||
{ name: 'idx_accounts_updated_at', columns: ['updated_at'] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Contact Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite-compatible ContactMethod type
|
|
||||||
*/
|
|
||||||
export interface SQLiteContactMethod {
|
|
||||||
id: string; // TEXT PRIMARY KEY
|
|
||||||
contact_id: string; // TEXT NOT NULL
|
|
||||||
label: string; // TEXT NOT NULL
|
|
||||||
type: string; // TEXT NOT NULL
|
|
||||||
value: string; // TEXT NOT NULL
|
|
||||||
created_at: number; // INTEGER NOT NULL
|
|
||||||
updated_at: number; // INTEGER NOT NULL
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite-compatible Contact type
|
|
||||||
*/
|
|
||||||
export interface SQLiteContact {
|
|
||||||
id: string; // TEXT PRIMARY KEY
|
|
||||||
did: string; // TEXT NOT NULL
|
|
||||||
name?: string; // TEXT
|
|
||||||
notes?: string; // TEXT
|
|
||||||
profile_image_url?: string; // TEXT
|
|
||||||
public_key_base64?: string; // TEXT
|
|
||||||
next_pub_key_hash_b64?: string; // TEXT
|
|
||||||
sees_me?: boolean; // INTEGER (0 or 1)
|
|
||||||
registered?: boolean; // INTEGER (0 or 1)
|
|
||||||
created_at: number; // INTEGER NOT NULL
|
|
||||||
updated_at: number; // INTEGER NOT NULL
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CONTACTS_TABLE: SQLiteTable = {
|
|
||||||
name: 'contacts',
|
|
||||||
columns: [
|
|
||||||
{ name: 'id', type: 'TEXT', primaryKey: true },
|
|
||||||
{ name: 'did', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'name', type: 'TEXT' },
|
|
||||||
{ name: 'notes', type: 'TEXT' },
|
|
||||||
{ name: 'profile_image_url', type: 'TEXT' },
|
|
||||||
{ name: 'public_key_base64', type: 'TEXT' },
|
|
||||||
{ name: 'next_pub_key_hash_b64', type: 'TEXT' },
|
|
||||||
{ name: 'sees_me', type: 'INTEGER' },
|
|
||||||
{ name: 'registered', type: 'INTEGER' },
|
|
||||||
{ name: 'created_at', type: 'INTEGER', nullable: false },
|
|
||||||
{ name: 'updated_at', type: 'INTEGER', nullable: false }
|
|
||||||
],
|
|
||||||
indexes: [
|
|
||||||
{ name: 'idx_contacts_did', columns: ['did'] },
|
|
||||||
{ name: 'idx_contacts_created_at', columns: ['created_at'] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CONTACT_METHODS_TABLE: SQLiteTable = {
|
|
||||||
name: 'contact_methods',
|
|
||||||
columns: [
|
|
||||||
{ name: 'id', type: 'TEXT', primaryKey: true },
|
|
||||||
{ name: 'contact_id', type: 'TEXT', nullable: false,
|
|
||||||
references: { table: 'contacts', column: 'id' } },
|
|
||||||
{ name: 'label', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'type', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'value', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'created_at', type: 'INTEGER', nullable: false },
|
|
||||||
{ name: 'updated_at', type: 'INTEGER', nullable: false }
|
|
||||||
],
|
|
||||||
indexes: [
|
|
||||||
{ name: 'idx_contact_methods_contact_id', columns: ['contact_id'] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Settings Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite-compatible Settings type
|
|
||||||
*/
|
|
||||||
export interface SQLiteSettings {
|
|
||||||
key: string; // TEXT PRIMARY KEY
|
|
||||||
account_did?: string; // TEXT
|
|
||||||
value_json: string; // TEXT NOT NULL (JSON stringified)
|
|
||||||
created_at: number; // INTEGER NOT NULL
|
|
||||||
updated_at: number; // INTEGER NOT NULL
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SETTINGS_TABLE: SQLiteTable = {
|
|
||||||
name: 'settings',
|
|
||||||
columns: [
|
|
||||||
{ name: 'key', type: 'TEXT', primaryKey: true },
|
|
||||||
{ name: 'account_did', type: 'TEXT' },
|
|
||||||
{ name: 'value_json', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'created_at', type: 'INTEGER', nullable: false },
|
|
||||||
{ name: 'updated_at', type: 'INTEGER', nullable: false }
|
|
||||||
],
|
|
||||||
indexes: [
|
|
||||||
{ name: 'idx_settings_account_did', columns: ['account_did'] },
|
|
||||||
{ name: 'idx_settings_updated_at', columns: ['updated_at'] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Log Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite-compatible Log type
|
|
||||||
*/
|
|
||||||
export interface SQLiteLog {
|
|
||||||
id: string; // TEXT PRIMARY KEY
|
|
||||||
level: string; // TEXT NOT NULL
|
|
||||||
message: string; // TEXT NOT NULL
|
|
||||||
metadata_json?: string; // TEXT (JSON stringified)
|
|
||||||
created_at: number; // INTEGER NOT NULL
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LOGS_TABLE: SQLiteTable = {
|
|
||||||
name: 'logs',
|
|
||||||
columns: [
|
|
||||||
{ name: 'id', type: 'TEXT', primaryKey: true },
|
|
||||||
{ name: 'level', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'message', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'metadata_json', type: 'TEXT' },
|
|
||||||
{ name: 'created_at', type: 'INTEGER', nullable: false }
|
|
||||||
],
|
|
||||||
indexes: [
|
|
||||||
{ name: 'idx_logs_level', columns: ['level'] },
|
|
||||||
{ name: 'idx_logs_created_at', columns: ['created_at'] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Secret Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQLite-compatible Secret type
|
|
||||||
* Note: This table should be encrypted at the database level
|
|
||||||
*/
|
|
||||||
export interface SQLiteSecret {
|
|
||||||
key: string; // TEXT PRIMARY KEY
|
|
||||||
value_encrypted: string; // TEXT NOT NULL (encrypted)
|
|
||||||
created_at: number; // INTEGER NOT NULL
|
|
||||||
updated_at: number; // INTEGER NOT NULL
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SECRETS_TABLE: SQLiteTable = {
|
|
||||||
name: 'secrets',
|
|
||||||
columns: [
|
|
||||||
{ name: 'key', type: 'TEXT', primaryKey: true },
|
|
||||||
{ name: 'value_encrypted', type: 'TEXT', nullable: false },
|
|
||||||
{ name: 'created_at', type: 'INTEGER', nullable: false },
|
|
||||||
{ name: 'updated_at', type: 'INTEGER', nullable: false }
|
|
||||||
],
|
|
||||||
indexes: [
|
|
||||||
{ name: 'idx_secrets_updated_at', columns: ['updated_at'] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Database Schema
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete database schema definition
|
|
||||||
*/
|
|
||||||
export const DATABASE_SCHEMA: SQLiteTable[] = [
|
|
||||||
ACCOUNTS_TABLE,
|
|
||||||
CONTACTS_TABLE,
|
|
||||||
CONTACT_METHODS_TABLE,
|
|
||||||
SETTINGS_TABLE,
|
|
||||||
LOGS_TABLE,
|
|
||||||
SECRETS_TABLE
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Type Guards and Validators
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard for SQLiteAccount
|
|
||||||
*/
|
|
||||||
export function isSQLiteAccount(value: unknown): value is SQLiteAccount {
|
|
||||||
return (
|
|
||||||
typeof value === 'object' &&
|
|
||||||
value !== null &&
|
|
||||||
typeof (value as SQLiteAccount).did === 'string' &&
|
|
||||||
typeof (value as SQLiteAccount).public_key_hex === 'string' &&
|
|
||||||
typeof (value as SQLiteAccount).created_at === 'number' &&
|
|
||||||
typeof (value as SQLiteAccount).updated_at === 'number'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard for SQLiteContact
|
|
||||||
*/
|
|
||||||
export function isSQLiteContact(value: unknown): value is SQLiteContact {
|
|
||||||
return (
|
|
||||||
typeof value === 'object' &&
|
|
||||||
value !== null &&
|
|
||||||
typeof (value as SQLiteContact).id === 'string' &&
|
|
||||||
typeof (value as SQLiteContact).did === 'string' &&
|
|
||||||
typeof (value as SQLiteContact).created_at === 'number' &&
|
|
||||||
typeof (value as SQLiteContact).updated_at === 'number'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard for SQLiteSettings
|
|
||||||
*/
|
|
||||||
export function isSQLiteSettings(value: unknown): value is SQLiteSettings {
|
|
||||||
return (
|
|
||||||
typeof value === 'object' &&
|
|
||||||
value !== null &&
|
|
||||||
typeof (value as SQLiteSettings).key === 'string' &&
|
|
||||||
typeof (value as SQLiteSettings).value_json === 'string' &&
|
|
||||||
typeof (value as SQLiteSettings).created_at === 'number' &&
|
|
||||||
typeof (value as SQLiteSettings).updated_at === 'number'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Migration Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type for migration data from Dexie to SQLite
|
|
||||||
*/
|
|
||||||
export interface MigrationData {
|
|
||||||
accounts: SQLiteAccount[];
|
|
||||||
contacts: SQLiteContact[];
|
|
||||||
contactMethods: SQLiteContactMethod[];
|
|
||||||
settings: SQLiteSettings[];
|
|
||||||
logs: SQLiteLog[];
|
|
||||||
secrets: SQLiteSecret[];
|
|
||||||
metadata: {
|
|
||||||
version: string;
|
|
||||||
timestamp: number;
|
|
||||||
source: 'dexie';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration result type
|
|
||||||
*/
|
|
||||||
export interface MigrationResult {
|
|
||||||
success: boolean;
|
|
||||||
error?: Error;
|
|
||||||
stats: {
|
|
||||||
accounts: number;
|
|
||||||
contacts: number;
|
|
||||||
contactMethods: number;
|
|
||||||
settings: number;
|
|
||||||
logs: number;
|
|
||||||
secrets: number;
|
|
||||||
};
|
|
||||||
duration: number;
|
|
||||||
}
|
|
||||||
@@ -7,11 +7,11 @@ export interface QueryExecResult {
|
|||||||
|
|
||||||
export interface DatabaseService {
|
export interface DatabaseService {
|
||||||
initialize(): Promise<void>;
|
initialize(): Promise<void>;
|
||||||
query(sql: string, params?: any[]): Promise<QueryExecResult[]>;
|
query(sql: string, params?: unknown[]): Promise<QueryExecResult[]>;
|
||||||
run(
|
run(
|
||||||
sql: string,
|
sql: string,
|
||||||
params?: any[],
|
params?: unknown[],
|
||||||
): Promise<{ changes: number; lastId?: number }>;
|
): Promise<{ changes: number; lastId?: number }>;
|
||||||
getOneRow(sql: string, params?: any[]): Promise<any[] | undefined>;
|
getOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
|
||||||
getAll(sql: string, params?: any[]): Promise<any[][]>;
|
getAll(sql: string, params?: unknown[]): Promise<unknown[][]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,7 +567,7 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
|||||||
|
|
||||||
await updateDefaultSettings({ activeDid: newId.did });
|
await updateDefaultSettings({ activeDid: newId.did });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update default settings:", error);
|
logger.error("Failed to update default settings:", error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to set default settings. Please try again or restart the app.",
|
"Failed to set default settings. Please try again or restart the app.",
|
||||||
);
|
);
|
||||||
|
|||||||
8
src/services/database.d.ts
vendored
8
src/services/database.d.ts
vendored
@@ -2,9 +2,9 @@ import { DatabaseService } from "../interfaces/database";
|
|||||||
|
|
||||||
declare module "@jlongster/sql.js" {
|
declare module "@jlongster/sql.js" {
|
||||||
interface SQL {
|
interface SQL {
|
||||||
Database: any;
|
Database: unknown;
|
||||||
FS: any;
|
FS: unknown;
|
||||||
register_for_idb: (fs: any) => void;
|
register_for_idb: (fs: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initSqlJs(config: {
|
function initSqlJs(config: {
|
||||||
@@ -15,7 +15,7 @@ declare module "@jlongster/sql.js" {
|
|||||||
|
|
||||||
declare module "absurd-sql" {
|
declare module "absurd-sql" {
|
||||||
export class SQLiteFS {
|
export class SQLiteFS {
|
||||||
constructor(fs: any, backend: any);
|
constructor(fs: unknown, backend: unknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend";
|
|||||||
|
|
||||||
import { runMigrations } from "../db-sql/migration";
|
import { runMigrations } from "../db-sql/migration";
|
||||||
import type { QueryExecResult } from "../interfaces/database";
|
import type { QueryExecResult } from "../interfaces/database";
|
||||||
|
import { logger } from "@/utils/logger";
|
||||||
|
|
||||||
interface SQLDatabase {
|
interface SQLDatabase {
|
||||||
exec: (sql: string, params?: any[]) => Promise<QueryExecResult[]>;
|
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||||
run: (
|
run: (
|
||||||
sql: string,
|
sql: string,
|
||||||
params?: any[],
|
params?: unknown[],
|
||||||
) => Promise<{ changes: number; lastId?: number }>;
|
) => Promise<{ changes: number; lastId?: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ class DatabaseService {
|
|||||||
try {
|
try {
|
||||||
await this.initializationPromise;
|
await this.initializationPromise;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`DatabaseService initialize method failed:`, error);
|
logger.error(`DatabaseService initialize method failed:`, error);
|
||||||
this.initializationPromise = null; // Reset on failure
|
this.initializationPromise = null; // Reset on failure
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ class DatabaseService {
|
|||||||
|
|
||||||
// If initialized but no db, something went wrong
|
// If initialized but no db, something went wrong
|
||||||
if (!this.db) {
|
if (!this.db) {
|
||||||
console.error(
|
logger.error(
|
||||||
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
||||||
);
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -128,25 +129,28 @@ class DatabaseService {
|
|||||||
// Used for inserts, updates, and deletes
|
// Used for inserts, updates, and deletes
|
||||||
async run(
|
async run(
|
||||||
sql: string,
|
sql: string,
|
||||||
params: any[] = [],
|
params: unknown[] = [],
|
||||||
): Promise<{ changes: number; lastId?: number }> {
|
): Promise<{ changes: number; lastId?: number }> {
|
||||||
await this.waitForInitialization();
|
await this.waitForInitialization();
|
||||||
return this.db!.run(sql, params);
|
return this.db!.run(sql, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that the resulting array may be empty if there are no results from the query
|
// Note that the resulting array may be empty if there are no results from the query
|
||||||
async query(sql: string, params: any[] = []): Promise<QueryExecResult[]> {
|
async query(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
||||||
await this.waitForInitialization();
|
await this.waitForInitialization();
|
||||||
return this.db!.exec(sql, params);
|
return this.db!.exec(sql, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOneRow(sql: string, params: any[] = []): Promise<any[] | undefined> {
|
async getOneRow(
|
||||||
|
sql: string,
|
||||||
|
params: unknown[] = [],
|
||||||
|
): Promise<unknown[] | undefined> {
|
||||||
await this.waitForInitialization();
|
await this.waitForInitialization();
|
||||||
const result = await this.db!.exec(sql, params);
|
const result = await this.db!.exec(sql, params);
|
||||||
return result[0]?.values[0];
|
return result[0]?.values[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async all(sql: string, params: any[] = []): Promise<any[][]> {
|
async all(sql: string, params: unknown[] = []): Promise<unknown[][]> {
|
||||||
await this.waitForInitialization();
|
await this.waitForInitialization();
|
||||||
const result = await this.db!.exec(sql, params);
|
const result = await this.db!.exec(sql, params);
|
||||||
return result[0]?.values || [];
|
return result[0]?.values || [];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logger from "@/utils/logger";
|
||||||
import { QueryExecResult } from "../interfaces/database";
|
import { QueryExecResult } from "../interfaces/database";
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
@@ -23,7 +24,10 @@ export class MigrationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async runMigrations(
|
async runMigrations(
|
||||||
sqlExec: (sql: string, params?: any[]) => Promise<Array<QueryExecResult>>,
|
sqlExec: (
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
) => Promise<Array<QueryExecResult>>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Create migrations table if it doesn't exist
|
// Create migrations table if it doesn't exist
|
||||||
await sqlExec(`
|
await sqlExec(`
|
||||||
@@ -43,7 +47,7 @@ export class MigrationService {
|
|||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
const singleResult = result[0];
|
const singleResult = result[0];
|
||||||
executedMigrations = new Set(
|
executedMigrations = new Set(
|
||||||
singleResult.values.map((row: any[]) => row[0]),
|
singleResult.values.map((row: unknown[]) => row[0]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +59,9 @@ export class MigrationService {
|
|||||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||||
migration.name,
|
migration.name,
|
||||||
]);
|
]);
|
||||||
console.log(`Migration ${migration.name} executed successfully`);
|
logger.log(`Migration ${migration.name} executed successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error executing migration ${migration.name}:`, error);
|
logger.error(`Error executing migration ${migration.name}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,10 @@
|
|||||||
<font-awesome icon="camera" class="fa-fw" />
|
<font-awesome icon="camera" class="fa-fw" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- If not registered, they don't need to see this at all. We show a prompt to register below. -->
|
<!--
|
||||||
|
If not registered, they don't need to see this at all. We show a prompt
|
||||||
|
to register below.
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
<ImageMethodDialog
|
<ImageMethodDialog
|
||||||
ref="imageMethodDialog"
|
ref="imageMethodDialog"
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default class NewIdentifierView extends Vue {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.hitError = true;
|
this.hitError = true;
|
||||||
console.error("Failed to generate identity:", error);
|
logger.error("Failed to generate identity:", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export default class Help extends Vue {
|
|||||||
|
|
||||||
// for SQL operations
|
// for SQL operations
|
||||||
sqlQuery = "";
|
sqlQuery = "";
|
||||||
sqlResult: any = null;
|
sqlResult: unknown = null;
|
||||||
|
|
||||||
cryptoLib = cryptoLib;
|
cryptoLib = cryptoLib;
|
||||||
|
|
||||||
@@ -542,9 +542,9 @@ export default class Help extends Vue {
|
|||||||
} else {
|
} else {
|
||||||
this.sqlResult = await databaseService.run(this.sqlQuery);
|
this.sqlResult = await databaseService.run(this.sqlQuery);
|
||||||
}
|
}
|
||||||
console.log("SQL Result:", this.sqlResult);
|
logger.log("SQL Result:", this.sqlResult);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("SQL Error:", error);
|
logger.error("SQL Error:", error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
Reference in New Issue
Block a user