/* * IndexedDB operations module. Provides typed helpers for reading and writing to * the browser-side database. All functions depend on `state.db` being set during * initialization. * * Changes from the original monolithic app.js: * - A5: `dbTransaction()` wraps multi-store operations in a single IndexedDB * transaction so related writes (e.g. delete report + delete attachments) are * atomic and won't leave orphaned records on mid-operation failure. */ import { state } from './state.js'; import { DB_NAME, DB_VERSION, STORE_TEMPLATES, STORE_LOOKUPS, STORE_CONFIG, STORE_REPORTS, STORE_ATTACHMENTS, STORE_SETTINGS } from './constants.js'; /* ── Database bootstrap ─────────────────────────────────────────────────── */ export function openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = (event) => { const db = request.result; if (!db.objectStoreNames.contains(STORE_TEMPLATES)) { db.createObjectStore(STORE_TEMPLATES, { keyPath: 'cacheKey' }); } if (!db.objectStoreNames.contains(STORE_LOOKUPS)) { db.createObjectStore(STORE_LOOKUPS, { keyPath: 'code' }); } if (!db.objectStoreNames.contains(STORE_CONFIG)) { db.createObjectStore(STORE_CONFIG, { keyPath: 'key' }); } if (!db.objectStoreNames.contains(STORE_REPORTS)) { db.createObjectStore(STORE_REPORTS, { keyPath: 'id' }); } if (!db.objectStoreNames.contains(STORE_ATTACHMENTS)) { const store = db.createObjectStore(STORE_ATTACHMENTS, { keyPath: 'id' }); store.createIndex('byReportId', 'reportId', { unique: false }); } if (!db.objectStoreNames.contains(STORE_SETTINGS)) { db.createObjectStore(STORE_SETTINGS, { keyPath: 'key' }); } }; }); } /* ── Single-store helpers ───────────────────────────────────────────────── */ export function dbGetAll(storeName) { return executeStoreRequest(storeName, 'readonly', (store) => store.getAll()); } export function dbGet(storeName, key) { return executeStoreRequest(storeName, 'readonly', (store) => store.get(key)); } export function dbPut(storeName, value) { return executeStoreRequest(storeName, 'readwrite', (store) => store.put(value)); } export function dbDelete(storeName, key) { return executeStoreRequest(storeName, 'readwrite', (store) => store.delete(key)); } export function dbGetAllByIndex(storeName, indexName, key) { return executeStoreRequest(storeName, 'readonly', (store) => { return store.index(indexName).getAll(IDBKeyRange.only(key)); }); } function executeStoreRequest(storeName, mode, callback) { return new Promise((resolve, reject) => { const transaction = state.db.transaction(storeName, mode); const store = transaction.objectStore(storeName); const request = callback(store); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } /* ── Multi-store transaction (A5) ───────────────────────────────────────── */ /* * Wraps multiple writes across different object stores in a single IndexedDB * transaction. The callback receives a helper that returns a store by name. * All writes either commit together or roll back as a unit. * * Example: * await dbTransaction([STORE_REPORTS, STORE_ATTACHMENTS], 'readwrite', (getStore) => { * getStore(STORE_REPORTS).delete(reportId); * getStore(STORE_ATTACHMENTS).delete(attachmentId); * }); */ export function dbTransaction(storeNames, mode, callback) { return new Promise((resolve, reject) => { const tx = state.db.transaction(storeNames, mode); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error || new Error('Transaction aborted')); const getStore = (name) => tx.objectStore(name); callback(getStore); }); } /* ── Settings helpers ───────────────────────────────────────────────────── */ export async function saveSetting(key, value) { await dbPut(STORE_SETTINGS, { key, value }); } export async function loadSetting(key) { const record = await dbGet(STORE_SETTINGS, key); return record?.value; }