import { Capacitor } from '@capacitor/core' import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite' import { defineCustomElements as defineJeep } from 'jeep-sqlite/loader' const DB_NAME = 'echo_local' const DB_VERSION = 1 const TRANSACTION_TABLE_SQL = ` CREATE TABLE IF NOT EXISTS transactions ( id TEXT PRIMARY KEY NOT NULL, amount REAL NOT NULL, merchant TEXT NOT NULL, category TEXT DEFAULT 'Uncategorized', date TEXT NOT NULL, note TEXT, sync_status INTEGER DEFAULT 0, entry_type TEXT DEFAULT 'expense', fund_source_type TEXT DEFAULT 'cash', fund_source_name TEXT, fund_target_type TEXT DEFAULT 'merchant', fund_target_name TEXT, impact_expense INTEGER DEFAULT 1, impact_income INTEGER DEFAULT 0, ai_category TEXT, ai_tags TEXT, ai_confidence REAL, ai_reason TEXT, ai_status TEXT DEFAULT 'idle', ai_model TEXT, ai_normalized_merchant TEXT ); ` const MERCHANT_PROFILE_TABLE_SQL = ` CREATE TABLE IF NOT EXISTS merchant_profiles ( merchant_key TEXT PRIMARY KEY NOT NULL, normalized_merchant TEXT NOT NULL, category TEXT, tags TEXT, hit_count INTEGER DEFAULT 0, updated_at TEXT NOT NULL ); ` const TRANSACTION_REQUIRED_COLUMNS = { entry_type: "TEXT DEFAULT 'expense'", fund_source_type: "TEXT DEFAULT 'cash'", fund_source_name: 'TEXT', fund_target_type: "TEXT DEFAULT 'merchant'", fund_target_name: 'TEXT', impact_expense: 'INTEGER DEFAULT 1', impact_income: 'INTEGER DEFAULT 0', ai_category: 'TEXT', ai_tags: 'TEXT', ai_confidence: 'REAL', ai_reason: 'TEXT', ai_status: "TEXT DEFAULT 'idle'", ai_model: 'TEXT', ai_normalized_merchant: 'TEXT', } let sqliteConnection let db let initialized = false let jeepLoaderPromise let jeepElementReadyPromise const waitFor = (ms = 100) => new Promise((resolve) => { setTimeout(resolve, ms) }) const ensureStoreReady = async (jeepEl, retries = 5) => { for (let attempt = 0; attempt < retries; attempt += 1) { const isOpen = (await jeepEl.isStoreOpen?.()) === true if (isOpen) return if (typeof jeepEl.openStore === 'function') { const opened = await jeepEl.openStore('jeepSqliteStore', 'databases') if (opened) return } await waitFor(200 * (attempt + 1)) } throw new Error('jeep-sqlite IndexedDB store failed to initialize') } const ensureJeepElement = async () => { if (!jeepLoaderPromise) { jeepLoaderPromise = defineJeep(window) } await jeepLoaderPromise if (!jeepElementReadyPromise) { jeepElementReadyPromise = (async () => { let jeepEl = document.querySelector('jeep-sqlite') if (!jeepEl) { jeepEl = document.createElement('jeep-sqlite') jeepEl.setAttribute('auto-save', 'true') document.body.appendChild(jeepEl) } jeepEl.setAttribute('wasm-path', '/assets') await customElements.whenDefined('jeep-sqlite') await jeepEl.componentOnReady?.() return jeepEl })() } const element = await jeepElementReadyPromise await ensureStoreReady(element) await CapacitorSQLite.initWebStore() } const ensureTableColumns = async (tableName, requiredColumns) => { const result = await db.query(`PRAGMA table_info(${tableName});`) const existingColumns = new Set((result?.values || []).map((row) => row.name)) for (const [columnName, definition] of Object.entries(requiredColumns)) { if (!existingColumns.has(columnName)) { await db.execute(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition};`) } } } const prepareConnection = async () => { if (!sqliteConnection) { sqliteConnection = new SQLiteConnection(CapacitorSQLite) } if (Capacitor.getPlatform() === 'web') { await ensureJeepElement() } const consistency = await sqliteConnection.checkConnectionsConsistency() if (!consistency.result) { await sqliteConnection.closeAllConnections() } const isConn = (await sqliteConnection.isConnection(DB_NAME, false)).result if (isConn) { db = await sqliteConnection.retrieveConnection(DB_NAME, false) } else { db = await sqliteConnection.createConnection(DB_NAME, false, 'no-encryption', DB_VERSION, false) } await db.open() await db.execute(TRANSACTION_TABLE_SQL) await db.execute(MERCHANT_PROFILE_TABLE_SQL) await ensureTableColumns('transactions', TRANSACTION_REQUIRED_COLUMNS) initialized = true } export const initSQLite = async () => { if (!initialized) { await prepareConnection() } return db } export const getDb = async () => { if (!initialized) { await initSQLite() } return db } export const saveDbToStore = async () => { await initSQLite() if (!sqliteConnection?.saveToStore) return try { await sqliteConnection.saveToStore(DB_NAME) } catch (error) { console.warn('[sqlite] saveToStore failed, data remains in memory until next sync', error) } } export const closeSQLite = async () => { if (db) { await db.close() db = null } initialized = false } export const databaseName = DB_NAME