stage 1
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user