134 lines
4.7 KiB
JavaScript
134 lines
4.7 KiB
JavaScript
/*
|
|
* 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;
|
|
}
|