aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/db.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/db.ts')
-rw-r--r--packages/taler-wallet-core/src/db.ts317
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();
});