Files
echo/src/lib/sqlite.js

167 lines
4.5 KiB
JavaScript
Raw Normal View History

import { Capacitor } from '@capacitor/core'
2025-11-25 17:12:09 +08:00
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,
ai_category TEXT,
ai_tags TEXT,
ai_confidence REAL,
ai_reason TEXT,
ai_status TEXT DEFAULT 'idle',
ai_model TEXT,
ai_normalized_merchant TEXT
2025-11-25 17:12:09 +08:00
);
`
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 = {
ai_category: 'TEXT',
ai_tags: 'TEXT',
ai_confidence: 'REAL',
ai_reason: 'TEXT',
ai_status: "TEXT DEFAULT 'idle'",
ai_model: 'TEXT',
ai_normalized_merchant: 'TEXT',
}
2025-11-25 17:12:09 +08:00
let sqliteConnection
let db
let initialized = false
2025-11-27 10:44:27 +08:00
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')
2025-11-27 10:44:27 +08:00
}
2025-11-25 17:12:09 +08:00
const ensureJeepElement = async () => {
2025-11-27 10:44:27 +08:00
if (!jeepLoaderPromise) {
jeepLoaderPromise = defineJeep(window)
2025-11-25 17:12:09 +08:00
}
2025-11-27 10:44:27 +08:00
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
})()
2025-11-25 17:12:09 +08:00
}
2025-11-27 10:44:27 +08:00
const element = await jeepElementReadyPromise
await ensureStoreReady(element)
2025-11-25 17:12:09 +08:00
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};`)
}
}
}
2025-11-25 17:12:09 +08:00
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)
2025-11-25 17:12:09 +08:00
initialized = true
}
export const initSQLite = async () => {
if (!initialized) {
await prepareConnection()
}
return db
}
export const getDb = async () => {
if (!initialized) {
await initSQLite()
}
return db
}
2025-11-27 10:44:27 +08:00
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)
2025-11-27 10:44:27 +08:00
}
}
2025-11-25 17:12:09 +08:00
export const closeSQLite = async () => {
if (db) {
await db.close()
db = null
}
initialized = false
}
export const databaseName = DB_NAME