diff options
Diffstat (limited to 'packages/taler-wallet-core/src/db.ts')
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 317 |
1 files changed, 219 insertions, 98 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 0fad66d92..b52a503bc 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -23,6 +23,7 @@ import { IDBFactory, IDBObjectStore, IDBTransaction, + structuredEncapsulate, } from "@gnu-taler/idb-bridge"; import { AgeCommitmentProof, @@ -103,7 +104,7 @@ import { RetryInfo, TaskIdentifiers } from "./operations/common.js"; * for all previous versions must be written, which should be * avoided. */ -export const TALER_DB_NAME = "taler-wallet-main-v9"; +export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v9"; /** * Name of the metadata database. This database is used @@ -111,7 +112,12 @@ export const TALER_DB_NAME = "taler-wallet-main-v9"; * * (Minor migrations are handled via upgrade transactions.) */ -export const TALER_META_DB_NAME = "taler-wallet-meta"; +export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; + +/** + * Stored backups, mainly created when manually importing a backup. + */ +export const TALER_WALLET_STORED_BACKUPS_DB_NAME = "taler-wallet-stored-backups"; export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; @@ -566,10 +572,31 @@ export interface ExchangeDetailsPointer { updateClock: TalerPreciseTimestamp; } +export enum ExchangeEntryDbRecordStatus { + Preset = 1, + Ephemeral = 2, + Used = 3, +} + +export enum ExchangeEntryDbUpdateStatus { + Initial = 1, + InitialUpdate = 2, + Suspended = 3, + Failed = 4, + OutdatedUpdate = 5, + Ready = 6, + ReadyUpdate = 7, +} + +/** + * Timestamp stored as a IEEE 754 double, in milliseconds. + */ +export type DbIndexableTimestampMs = number; + /** * Exchange record as stored in the wallet's database. */ -export interface ExchangeRecord { +export interface ExchangeEntryRecord { /** * Base url of the exchange. */ @@ -594,13 +621,12 @@ export interface ExchangeRecord { */ detailsPointer: ExchangeDetailsPointer | undefined; - /** - * Is this a permanent or temporary exchange record? - */ - permanent: boolean; + entryStatus: ExchangeEntryDbRecordStatus; + + updateStatus: ExchangeEntryDbUpdateStatus; /** - * Last time when the exchange was updated (both /keys and /wire). + * Last time when the exchange /keys info was updated. */ lastUpdate: TalerPreciseTimestamp | undefined; @@ -608,20 +634,21 @@ export interface ExchangeRecord { * Next scheduled update for the exchange. * * (This field must always be present, so we can index on the timestamp.) + * + * FIXME: To index on the timestamp, this needs to be a number of + * binary timestamp! */ - nextUpdate: TalerPreciseTimestamp; + nextUpdateStampMs: DbIndexableTimestampMs; lastKeysEtag: string | undefined; - lastWireEtag: string | undefined; - /** * Next time that we should check if coins need to be refreshed. * * Updated whenever the exchange's denominations are updated or when * the refresh check has been done. */ - nextRefreshCheck: TalerPreciseTimestamp; + nextRefreshCheckStampMs: DbIndexableTimestampMs; /** * Public key of the reserve that we're currently using for @@ -2431,7 +2458,7 @@ export const WalletStoresV1 = { ), exchanges: describeStore( "exchanges", - describeContents<ExchangeRecord>({ + describeContents<ExchangeEntryRecord>({ keyPath: "baseUrl", }), {}, @@ -2725,11 +2752,10 @@ export type WalletDbReadOnlyTransaction< Stores extends StoreNames<typeof WalletStoresV1> & string, > = DbReadOnlyTransaction<typeof WalletStoresV1, Stores>; -export type WalletReadWriteTransaction< +export type WalletDbReadWriteTransaction< Stores extends StoreNames<typeof WalletStoresV1> & string, > = DbReadWriteTransaction<typeof WalletStoresV1, Stores>; - /** * An applied migration. */ @@ -2760,45 +2786,144 @@ export const walletMetadataStore = { ), }; -export function exportDb(db: IDBDatabase): Promise<any> { - const dump = { - name: db.name, - stores: {} as { [s: string]: any }, - version: db.version, +export interface StoredBackupMeta { + name: string; +} + +export const StoredBackupStores = { + backupMeta: describeStore( + "backupMeta", + describeContents<StoredBackupMeta>({ keyPath: "name" }), + {}, + ), + backupData: describeStore("backupData", describeContents<any>({}), {}), +}; + +export interface DbDumpRecord { + /** + * Key, serialized with structuredEncapsulated. + * + * Only present for out-of-line keys (i.e. no key path). + */ + key?: any; + /** + * Value, serialized with structuredEncapsulated. + */ + value: any; +} + +export interface DbIndexDump { + keyPath: string | string[]; + multiEntry: boolean; + unique: boolean; +} + +export interface DbStoreDump { + keyPath?: string | string[]; + autoIncrement: boolean; + indexes: { [indexName: string]: DbIndexDump }; + records: DbDumpRecord[]; +} + +export interface DbDumpDatabase { + version: number; + stores: { [storeName: string]: DbStoreDump }; +} + +export interface DbDump { + databases: { + [name: string]: DbDumpDatabase; + }; +} + +export async function exportSingleDb( + idb: IDBFactory, + dbName: string, +): Promise<DbDumpDatabase> { + const myDb = await openDatabase( + idb, + dbName, + undefined, + () => { + // May not happen, since we're not requesting a specific version + throw Error("unexpected version change"); + }, + () => { + logger.info("unexpected onupgradeneeded"); + }, + ); + + const singleDbDump: DbDumpDatabase = { + version: myDb.version, + stores: {}, }; return new Promise((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames)); + const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); tx.addEventListener("complete", () => { - resolve(dump); + myDb.close(); + resolve(singleDbDump); }); // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < db.objectStoreNames.length; i++) { - const name = db.objectStoreNames[i]; - const storeDump = {} as { [s: string]: any }; - dump.stores[name] = storeDump; - tx.objectStore(name) - .openCursor() - .addEventListener("success", (e: Event) => { - const cursor = (e.target as any).result; - if (cursor) { - storeDump[cursor.key] = cursor.value; - cursor.continue(); + for (let i = 0; i < myDb.objectStoreNames.length; i++) { + const name = myDb.objectStoreNames[i]; + const store = tx.objectStore(name); + const storeDump: DbStoreDump = { + autoIncrement: store.autoIncrement, + keyPath: store.keyPath, + indexes: {}, + records: [], + }; + const indexNames = store.indexNames; + for (let j = 0; j < indexNames.length; j++) { + const idxName = indexNames[j]; + const index = store.index(idxName); + storeDump.indexes[idxName] = { + keyPath: index.keyPath, + multiEntry: index.multiEntry, + unique: index.unique, + }; + } + singleDbDump.stores[name] = storeDump; + store.openCursor().addEventListener("success", (e: Event) => { + const cursor = (e.target as any).result; + if (cursor) { + const rec: DbDumpRecord = { + value: structuredEncapsulate(cursor.value), + }; + // Only store key if necessary, i.e. when + // the key is not stored as part of the object via + // a key path. + if (store.keyPath == null) { + rec.key = structuredEncapsulate(cursor.key); } - }); + cursor.continue(); + } + }); } }); } -export interface DatabaseDump { - name: string; - stores: { [s: string]: any }; - version: string; +export async function exportDb(idb: IDBFactory): Promise<DbDump> { + const dbDump: DbDump = { + databases: {}, + }; + + dbDump.databases[TALER_WALLET_META_DB_NAME] = await exportSingleDb( + idb, + TALER_WALLET_META_DB_NAME, + ); + dbDump.databases[TALER_WALLET_MAIN_DB_NAME] = await exportSingleDb( + idb, + TALER_WALLET_MAIN_DB_NAME, + ); + + return dbDump; } async function recoverFromDump( db: IDBDatabase, - dump: DatabaseDump, + dbDump: DbDumpDatabase, ): Promise<void> { return new Promise((resolve, reject) => { const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); @@ -2807,67 +2932,33 @@ async function recoverFromDump( }); for (let i = 0; i < db.objectStoreNames.length; i++) { const name = db.objectStoreNames[i]; - const storeDump = dump.stores[name]; + const storeDump = dbDump.stores[name]; if (!storeDump) continue; - Object.keys(storeDump).forEach(async (key) => { - const value = storeDump[key]; - if (!value) return; - tx.objectStore(name).put(value); - }); + for (let rec of storeDump.records) { + tx.objectStore(name).put(rec.value, rec.key); + } } tx.commit(); }); } -export async function importDb(db: IDBDatabase, object: any): Promise<void> { - if ("name" in object && "stores" in object && "version" in object) { - // looks like a database dump - const dump = object as DatabaseDump; - return recoverFromDump(db, dump); - } - - if ("databases" in object && "$types" in object) { - // looks like a IDBDatabase - const someDatabase = object.databases; - - if (TALER_META_DB_NAME in someDatabase) { - //looks like a taler database - const currentMainDbValue = - someDatabase[TALER_META_DB_NAME].objectStores.metaConfig.records[0] - .value.value; - - if (currentMainDbValue !== TALER_DB_NAME) { - console.log("not the current database version"); - } - - const talerDb = someDatabase[currentMainDbValue]; - - const objectStoreNames = Object.keys(talerDb.objectStores); - - const dump: DatabaseDump = { - name: talerDb.schema.databaseName, - version: talerDb.schema.databaseVersion, - stores: {}, - }; - - for (let i = 0; i < objectStoreNames.length; i++) { - const name = objectStoreNames[i]; - const storeDump = {} as { [s: string]: any }; - dump.stores[name] = storeDump; - talerDb.objectStores[name].records.map((r: any) => { - const pkey = r.primaryKey; - const key = - typeof pkey === "string" || typeof pkey === "number" - ? pkey - : pkey.join(","); - storeDump[key] = r.value; - }); - } +function checkDbDump(x: any): x is DbDump { + return "databases" in x; +} - return recoverFromDump(db, dump); +export async function importDb(db: IDBDatabase, dumpJson: any): Promise<void> { + const d = dumpJson; + if (checkDbDump(d)) { + const walletDb = d.databases[TALER_WALLET_MAIN_DB_NAME]; + if (!walletDb) { + throw Error( + `unable to import, main wallet database (${TALER_WALLET_MAIN_DB_NAME}) not found`, + ); } + await recoverFromDump(db, walletDb); + } else { + throw Error("unable to import, doesn't look like a valid DB dump"); } - throw Error("could not import database"); } export interface FixupDescription { @@ -3151,6 +3242,36 @@ function onMetaDbUpgradeNeeded( ); } +function onStoredBackupsDbUpgradeNeeded( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +) { + upgradeFromStoreMap( + StoredBackupStores, + db, + oldVersion, + newVersion, + upgradeTransaction, + ); +} + +export async function openStoredBackupsDatabase( + idbFactory: IDBFactory, +): Promise<DbAccess<typeof StoredBackupStores>> { + const backupsDbHandle = await openDatabase( + idbFactory, + TALER_WALLET_STORED_BACKUPS_DB_NAME, + 1, + () => {}, + onStoredBackupsDbUpgradeNeeded, + ); + + const handle = new DbAccess(backupsDbHandle, StoredBackupStores); + return handle; +} + /** * Return a promise that resolves * to the taler wallet db. @@ -3164,7 +3285,7 @@ export async function openTalerDatabase( ): Promise<DbAccess<typeof WalletStoresV1>> { const metaDbHandle = await openDatabase( idbFactory, - TALER_META_DB_NAME, + TALER_WALLET_META_DB_NAME, 1, () => {}, onMetaDbUpgradeNeeded, @@ -3177,17 +3298,17 @@ export async function openTalerDatabase( .runReadWrite(async (tx) => { const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY); if (!dbVersionRecord) { - currentMainVersion = TALER_DB_NAME; + currentMainVersion = TALER_WALLET_MAIN_DB_NAME; await tx.metaConfig.put({ key: CURRENT_DB_CONFIG_KEY, - value: TALER_DB_NAME, + value: TALER_WALLET_MAIN_DB_NAME, }); } else { currentMainVersion = dbVersionRecord.value; } }); - if (currentMainVersion !== TALER_DB_NAME) { + if (currentMainVersion !== TALER_WALLET_MAIN_DB_NAME) { switch (currentMainVersion) { case "taler-wallet-main-v2": case "taler-wallet-main-v3": @@ -3203,7 +3324,7 @@ export async function openTalerDatabase( .runReadWrite(async (tx) => { await tx.metaConfig.put({ key: CURRENT_DB_CONFIG_KEY, - value: TALER_DB_NAME, + value: TALER_WALLET_MAIN_DB_NAME, }); }); break; @@ -3216,7 +3337,7 @@ export async function openTalerDatabase( const mainDbHandle = await openDatabase( idbFactory, - TALER_DB_NAME, + TALER_WALLET_MAIN_DB_NAME, WALLET_DB_MINOR_VERSION, onVersionChange, onTalerDbUpgradeNeeded, @@ -3233,7 +3354,7 @@ export async function deleteTalerDatabase( idbFactory: IDBFactory, ): Promise<void> { return new Promise((resolve, reject) => { - const req = idbFactory.deleteDatabase(TALER_DB_NAME); + const req = idbFactory.deleteDatabase(TALER_WALLET_MAIN_DB_NAME); req.onerror = () => reject(req.error); req.onsuccess = () => resolve(); }); |