aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/db.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-01-11 14:19:24 +0100
committerFlorian Dold <florian@dold.me>2023-01-11 14:19:24 +0100
commit668d7a213e21a776958d985b0758495d967d9f73 (patch)
tree2bef5a53c28be5f9d996e5dc7ed3833e89338de8 /packages/taler-wallet-core/src/db.ts
parenta82d8fab696d3fca24c2f1c48a1646107e38cef8 (diff)
downloadwallet-core-668d7a213e21a776958d985b0758495d967d9f73.tar.xz
wallet-core: implement database fixups
Diffstat (limited to 'packages/taler-wallet-core/src/db.ts')
-rw-r--r--packages/taler-wallet-core/src/db.ts281
1 files changed, 278 insertions, 3 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index c56c3a9b5..ef44adc96 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -17,7 +17,12 @@
/**
* Imports.
*/
-import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
+import {
+ Event,
+ IDBDatabase,
+ IDBFactory,
+ IDBTransaction,
+} from "@gnu-taler/idb-bridge";
import {
AgeCommitmentProof,
AmountJson,
@@ -51,13 +56,21 @@ import {
AttentionPriority,
AttentionInfo,
AbsoluteTime,
+ Logger,
} from "@gnu-taler/taler-util";
import {
+ DbAccess,
describeContents,
describeIndex,
describeStore,
+ GetReadWriteAccess,
+ IndexDescriptor,
+ openDatabase,
+ StoreDescriptor,
+ StoreWithIndexes,
} from "./util/query.js";
import { RetryInfo, RetryTags } from "./util/retries.js";
+import { Wallet } from "./wallet.js";
/**
* This file contains the database schema of the Taler wallet together
@@ -106,7 +119,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
* backwards-compatible way or object stores and indices
* are added.
*/
-export const WALLET_DB_MINOR_VERSION = 1;
+export const WALLET_DB_MINOR_VERSION = 2;
/**
* Ranges for operation status fields.
@@ -1327,7 +1340,6 @@ export type WgInfo =
| WgInfoBankPeerPush
| WgInfoBankRecoup;
-
export interface WithdrawalKycPendingInfo {
paytoHash: string;
requirementRow: number;
@@ -2183,12 +2195,28 @@ export const WalletStoresV1 = {
"userAttention",
describeContents<UserAttentionRecord>({
keyPath: ["entityId", "info.type"],
+ versionAdded: 2,
+ }),
+ {},
+ ),
+ fixups: describeStore(
+ "fixups",
+ describeContents<FixupRecord>({
+ keyPath: "fixupName",
+ versionAdded: 2,
}),
{},
),
};
/**
+ * An applied migration.
+ */
+export interface FixupRecord {
+ fixupName: string;
+}
+
+/**
* User accounts
*/
export interface BankAccountsRecord {
@@ -2320,3 +2348,250 @@ export async function importDb(db: IDBDatabase, object: any): Promise<void> {
}
throw Error("could not import database");
}
+
+export interface FixupDescription {
+ name: string;
+ fn(tx: GetReadWriteAccess<typeof WalletStoresV1>): Promise<void>;
+}
+
+/**
+ * Manual migrations between minor versions of the DB schema.
+ */
+export const walletDbFixups: FixupDescription[] = [
+ {
+ name: "RefreshGroupRecord_currency",
+ async fn(tx): Promise<void> {
+ await tx.refreshGroups.iter().forEachAsync(async (rg) => {
+ if (rg.currency) {
+ return;
+ }
+ // Empty refresh group without input coin, delete it!
+ if (rg.inputPerCoin.length === 0) {
+ await tx.refreshGroups.delete(rg.refreshGroupId);
+ return;
+ }
+ rg.currency = Amounts.parseOrThrow(rg.inputPerCoin[0]).currency;
+ await tx.refreshGroups.put(rg);
+ });
+ },
+ },
+];
+
+const logger = new Logger("db.ts");
+
+export async function applyFixups(
+ db: DbAccess<typeof WalletStoresV1>,
+): Promise<void> {
+ await db.mktxAll().runReadWrite(async (tx) => {
+ for (const fixupInstruction of walletDbFixups) {
+ const fixupRecord = await tx.fixups.get(fixupInstruction.name);
+ if (fixupRecord) {
+ return;
+ }
+ logger.info(`applying DB fixup ${fixupInstruction.name}`);
+ await fixupInstruction.fn(tx);
+ }
+ });
+}
+
+function upgradeFromStoreMap(
+ storeMap: any,
+ db: IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ upgradeTransaction: IDBTransaction,
+): void {
+ if (oldVersion === 0) {
+ for (const n in storeMap) {
+ const swi: StoreWithIndexes<
+ any,
+ StoreDescriptor<unknown>,
+ any
+ > = storeMap[n];
+ const storeDesc: StoreDescriptor<unknown> = swi.store;
+ const s = db.createObjectStore(swi.storeName, {
+ autoIncrement: storeDesc.autoIncrement,
+ keyPath: storeDesc.keyPath,
+ });
+ for (const indexName in swi.indexMap as any) {
+ const indexDesc: IndexDescriptor = swi.indexMap[indexName];
+ s.createIndex(indexDesc.name, indexDesc.keyPath, {
+ multiEntry: indexDesc.multiEntry,
+ unique: indexDesc.unique,
+ });
+ }
+ }
+ return;
+ }
+ if (oldVersion === newVersion) {
+ return;
+ }
+ logger.info(`upgrading database from ${oldVersion} to ${newVersion}`);
+ for (const n in storeMap) {
+ const swi: StoreWithIndexes<any, StoreDescriptor<unknown>, any> = storeMap[
+ n
+ ];
+ const storeDesc: StoreDescriptor<unknown> = swi.store;
+ const storeAddedVersion = storeDesc.versionAdded ?? 0;
+ if (storeAddedVersion <= oldVersion) {
+ continue;
+ }
+ const s = db.createObjectStore(swi.storeName, {
+ autoIncrement: storeDesc.autoIncrement,
+ keyPath: storeDesc.keyPath,
+ });
+ for (const indexName in swi.indexMap as any) {
+ const indexDesc: IndexDescriptor = swi.indexMap[indexName];
+ const indexAddedVersion = indexDesc.versionAdded ?? 0;
+ if (indexAddedVersion <= oldVersion) {
+ continue;
+ }
+ s.createIndex(indexDesc.name, indexDesc.keyPath, {
+ multiEntry: indexDesc.multiEntry,
+ unique: indexDesc.unique,
+ });
+ }
+ }
+}
+
+function promiseFromTransaction(transaction: IDBTransaction): Promise<void> {
+ return new Promise<void>((resolve, reject) => {
+ transaction.oncomplete = () => {
+ resolve();
+ };
+ transaction.onerror = () => {
+ reject();
+ };
+ });
+}
+
+/**
+ * Purge all data in the given database.
+ */
+export function clearDatabase(db: IDBDatabase): Promise<void> {
+ // db.objectStoreNames is a DOMStringList, so we need to convert
+ let stores: string[] = [];
+ for (let i = 0; i < db.objectStoreNames.length; i++) {
+ stores.push(db.objectStoreNames[i]);
+ }
+ const tx = db.transaction(stores, "readwrite");
+ for (const store of stores) {
+ tx.objectStore(store).clear();
+ }
+ return promiseFromTransaction(tx);
+}
+
+function onTalerDbUpgradeNeeded(
+ db: IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ upgradeTransaction: IDBTransaction,
+) {
+ upgradeFromStoreMap(
+ WalletStoresV1,
+ db,
+ oldVersion,
+ newVersion,
+ upgradeTransaction,
+ );
+}
+
+function onMetaDbUpgradeNeeded(
+ db: IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ upgradeTransaction: IDBTransaction,
+) {
+ upgradeFromStoreMap(
+ walletMetadataStore,
+ db,
+ oldVersion,
+ newVersion,
+ upgradeTransaction,
+ );
+}
+
+/**
+ * Return a promise that resolves
+ * to the taler wallet db.
+ */
+export async function openTalerDatabase(
+ idbFactory: IDBFactory,
+ onVersionChange: () => void,
+): Promise<DbAccess<typeof WalletStoresV1>> {
+ const metaDbHandle = await openDatabase(
+ idbFactory,
+ TALER_META_DB_NAME,
+ 1,
+ () => {},
+ onMetaDbUpgradeNeeded,
+ );
+
+ const metaDb = new DbAccess(metaDbHandle, walletMetadataStore);
+ let currentMainVersion: string | undefined;
+ await metaDb
+ .mktx((stores) => [stores.metaConfig])
+ .runReadWrite(async (tx) => {
+ const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY);
+ if (!dbVersionRecord) {
+ currentMainVersion = TALER_DB_NAME;
+ await tx.metaConfig.put({
+ key: CURRENT_DB_CONFIG_KEY,
+ value: TALER_DB_NAME,
+ });
+ } else {
+ currentMainVersion = dbVersionRecord.value;
+ }
+ });
+
+ if (currentMainVersion !== TALER_DB_NAME) {
+ switch (currentMainVersion) {
+ case "taler-wallet-main-v2":
+ case "taler-wallet-main-v3":
+ case "taler-wallet-main-v4": // temporary, we might migrate v4 later
+ case "taler-wallet-main-v5":
+ case "taler-wallet-main-v6":
+ case "taler-wallet-main-v7":
+ case "taler-wallet-main-v8":
+ // We consider this a pre-release
+ // development version, no migration is done.
+ await metaDb
+ .mktx((stores) => [stores.metaConfig])
+ .runReadWrite(async (tx) => {
+ await tx.metaConfig.put({
+ key: CURRENT_DB_CONFIG_KEY,
+ value: TALER_DB_NAME,
+ });
+ });
+ break;
+ default:
+ throw Error(
+ `major migration from database major=${currentMainVersion} not supported`,
+ );
+ }
+ }
+
+ const mainDbHandle = await openDatabase(
+ idbFactory,
+ TALER_DB_NAME,
+ WALLET_DB_MINOR_VERSION,
+ onVersionChange,
+ onTalerDbUpgradeNeeded,
+ );
+
+ const handle = new DbAccess(mainDbHandle, WalletStoresV1);
+
+ await applyFixups(handle);
+
+ return handle;
+}
+
+export async function deleteTalerDatabase(
+ idbFactory: IDBFactory,
+): Promise<void> {
+ return new Promise((resolve, reject) => {
+ const req = idbFactory.deleteDatabase(TALER_DB_NAME);
+ req.onerror = () => reject(req.error);
+ req.onsuccess = () => resolve();
+ });
+}