Browse Source
			
			
			
			
				
		- 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.sql-absurd-sql
				 14 changed files with 56 additions and 1510 deletions
			
			
		| @ -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; | |||
| }  | |||
					Loading…
					
					
				
		Reference in new issue