/** * 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( operation: (db: Database) => Promise ): Promise { 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( operation: () => Promise, maxRetries = 3, delay = 1000 ): Promise { 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 { const { db } = await initDatabase(); const accounts = await db.selectAll( 'SELECT * FROM accounts WHERE did = ?', [did] ); return accounts[0] || null; } /** * Get all accounts */ export async function getAllAccounts(): Promise { const { db } = await initDatabase(); return db.selectAll( 'SELECT * FROM accounts ORDER BY created_at DESC' ); } /** * Create or update account */ export async function upsertAccount(account: SQLiteAccount): Promise { 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 { const { db } = await initDatabase(); const contacts = await db.selectAll( 'SELECT * FROM contacts WHERE id = ?', [id] ); return contacts[0] || null; } /** * Get contacts by account DID */ export async function getContactsByAccountDid(did: string): Promise { const { db } = await initDatabase(); return db.selectAll( 'SELECT * FROM contacts WHERE did = ? ORDER BY created_at DESC', [did] ); } /** * Get contact methods for a contact */ export async function getContactMethods(contactId: string): Promise { const { db } = await initDatabase(); return db.selectAll( '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 { 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 { const { db } = await initDatabase(); const settings = await db.selectAll( 'SELECT * FROM settings WHERE key = ?', [key] ); return settings[0] || null; } /** * Get settings by account DID */ export async function getSettingsByAccountDid(did: string): Promise { const { db } = await initDatabase(); return db.selectAll( 'SELECT * FROM settings WHERE account_did = ? ORDER BY updated_at DESC', [did] ); } /** * Set setting value */ export async function setSetting(setting: SQLiteSettings): Promise { 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 { 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 { const { db } = await initDatabase(); return db.selectAll( '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 { const { db } = await initDatabase(); const secrets = await db.selectAll( 'SELECT * FROM secrets WHERE key = ?', [key] ); return secrets[0] || null; } /** * Set secret value */ export async function setSecret(secret: SQLiteSecret): Promise { 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 ]); } }); }