From 4e481a51c64084db21d3eea513b13a7a3bd6603a Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 26 Nov 2020 22:14:46 +0100 Subject: more static typing for transactions (fixes #6653) --- packages/taler-wallet-core/src/operations/tip.ts | 2 +- packages/taler-wallet-core/src/types/dbTypes.ts | 130 ++++++++++++++--------- packages/taler-wallet-core/src/util/query.ts | 127 ++++++++++++++-------- 3 files changed, 164 insertions(+), 95 deletions(-) (limited to 'packages') diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index cbf61a86c..bf565b9b2 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -285,7 +285,7 @@ async function processTipImpl( ); if (!isValid) { - await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => { + await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => { const tipRecord = await tx.get(Stores.tips, walletTipId); if (!tipRecord) { return; diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts index ed3a18ae1..349713ebc 100644 --- a/packages/taler-wallet-core/src/types/dbTypes.ts +++ b/packages/taler-wallet-core/src/types/dbTypes.ts @@ -785,7 +785,7 @@ export interface CoinRecord { /** * Blinding key used when withdrawing the coin. - * Potentionally sed again during payback. + * Potentionally used again during payback. */ blindingKey: string; @@ -1531,135 +1531,160 @@ export enum ImportPayloadType { CoreSchema = "core-schema", } -class ExchangesStore extends Store { +class ExchangesStore extends Store<"exchanges", ExchangeRecord> { constructor() { super("exchanges", { keyPath: "baseUrl" }); } } -class CoinsStore extends Store { +class CoinsStore extends Store<"coins", CoinRecord> { constructor() { super("coins", { keyPath: "coinPub" }); } - exchangeBaseUrlIndex = new Index( - this, - "exchangeBaseUrl", + exchangeBaseUrlIndex = new Index< + "coins", "exchangeBaseUrl", - ); - denomPubHashIndex = new Index( - this, + string, + CoinRecord + >(this, "exchangeBaseUrl", "exchangeBaseUrl"); + + denomPubHashIndex = new Index< + "coins", "denomPubHashIndex", - "denomPubHash", - ); + string, + CoinRecord + >(this, "denomPubHashIndex", "denomPubHash"); } -class ProposalsStore extends Store { +class ProposalsStore extends Store<"proposals", ProposalRecord> { constructor() { super("proposals", { keyPath: "proposalId" }); } - urlAndOrderIdIndex = new Index(this, "urlIndex", [ - "merchantBaseUrl", - "orderId", - ]); + urlAndOrderIdIndex = new Index< + "proposals", + "urlIndex", + string, + ProposalRecord + >(this, "urlIndex", ["merchantBaseUrl", "orderId"]); } -class PurchasesStore extends Store { +class PurchasesStore extends Store<"purchases", PurchaseRecord> { constructor() { super("purchases", { keyPath: "proposalId" }); } - fulfillmentUrlIndex = new Index( - this, + fulfillmentUrlIndex = new Index< + "purchases", "fulfillmentUrlIndex", - "contractData.fulfillmentUrl", + string, + PurchaseRecord + >(this, "fulfillmentUrlIndex", "contractData.fulfillmentUrl"); + + orderIdIndex = new Index<"purchases", "orderIdIndex", string, PurchaseRecord>( + this, + "orderIdIndex", + ["contractData.merchantBaseUrl", "contractData.orderId"], ); - orderIdIndex = new Index(this, "orderIdIndex", [ - "contractData.merchantBaseUrl", - "contractData.orderId", - ]); } -class DenominationsStore extends Store { +class DenominationsStore extends Store<"denominations", DenominationRecord> { constructor() { // cast needed because of bug in type annotations super("denominations", { keyPath: (["exchangeBaseUrl", "denomPubHash"] as any) as IDBKeyPath, }); } - exchangeBaseUrlIndex = new Index( - this, + exchangeBaseUrlIndex = new Index< + "denominations", "exchangeBaseUrlIndex", - "exchangeBaseUrl", - ); + string, + DenominationRecord + >(this, "exchangeBaseUrlIndex", "exchangeBaseUrl"); } -class CurrenciesStore extends Store { +class CurrenciesStore extends Store<"currencies", CurrencyRecord> { constructor() { super("currencies", { keyPath: "name" }); } } -class ConfigStore extends Store { +class ConfigStore extends Store<"config", ConfigRecord> { constructor() { super("config", { keyPath: "key" }); } } -class ReservesStore extends Store { +class ReservesStore extends Store<"reserves", ReserveRecord> { constructor() { super("reserves", { keyPath: "reservePub" }); } } -class ReserveHistoryStore extends Store { +class ReserveHistoryStore extends Store< + "reserveHistory", + ReserveHistoryRecord +> { constructor() { super("reserveHistory", { keyPath: "reservePub" }); } } -class TipsStore extends Store { +class TipsStore extends Store<"tips", TipRecord> { constructor() { super("tips", { keyPath: "walletTipId" }); } // Added in version 2 - byMerchantTipIdAndBaseUrl = new Index<[string, string], TipRecord>( + byMerchantTipIdAndBaseUrl = new Index< + "tips", + "tipsByMerchantTipIdAndOriginIndex", + [string, string], + TipRecord + >( this, "tipsByMerchantTipIdAndOriginIndex", ["merchantTipId", "merchantBaseUrl"], { versionAdded: 2, - } + }, ); } -class WithdrawalGroupsStore extends Store { +class WithdrawalGroupsStore extends Store< + "withdrawals", + WithdrawalGroupRecord +> { constructor() { super("withdrawals", { keyPath: "withdrawalGroupId" }); } } -class PlanchetsStore extends Store { +class PlanchetsStore extends Store<"planchets", PlanchetRecord> { constructor() { super("planchets", { keyPath: "coinPub" }); } - byGroupAndIndex = new Index( - this, + byGroupAndIndex = new Index< + "planchets", "withdrawalGroupAndCoinIdxIndex", - ["withdrawalGroupId", "coinIdx"], - ); - byGroup = new Index( - this, + string, + PlanchetRecord + >(this, "withdrawalGroupAndCoinIdxIndex", ["withdrawalGroupId", "coinIdx"]); + byGroup = new Index< + "planchets", "withdrawalGroupIndex", - "withdrawalGroupId", - ); + string, + PlanchetRecord + >(this, "withdrawalGroupIndex", "withdrawalGroupId"); } /** * This store is effectively a materialized index for * reserve records that are for a bank-integrated withdrawal. */ -class BankWithdrawUrisStore extends Store { +class BankWithdrawUrisStore extends Store< + "bankWithdrawUris", + BankWithdrawUriRecord +> { constructor() { super("bankWithdrawUris", { keyPath: "talerWithdrawUri" }); } @@ -1675,10 +1700,13 @@ export const Stores = { denominations: new DenominationsStore(), exchanges: new ExchangesStore(), proposals: new ProposalsStore(), - refreshGroups: new Store("refreshGroups", { - keyPath: "refreshGroupId", - }), - recoupGroups: new Store("recoupGroups", { + refreshGroups: new Store<"refreshGroups", RefreshGroupRecord>( + "refreshGroups", + { + keyPath: "refreshGroupId", + }, + ), + recoupGroups: new Store<"recoupGroups", RecoupGroupRecord>("recoupGroups", { keyPath: "recoupGroupId", }), reserves: new ReservesStore(), diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index f533c4cfd..fa0d8beb7 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -59,11 +59,8 @@ export interface StoreParams { /** * Definition of an object store. */ -export class Store { - constructor( - public name: string, - public storeParams?: StoreParams, - ) {} +export class Store { + constructor(public name: N, public storeParams?: StoreParams) {} } /** @@ -273,26 +270,48 @@ class ResultStream { } } -export class TransactionHandle { +type StrKey = string & keyof T; + +type StoreName = S extends Store ? N : never; +type StoreContent = S extends Store ? R : never; +type IndexRecord = Ind extends Index ? R : never; + +export class TransactionHandle> { constructor(private tx: IDBTransaction) {} - put(store: Store, value: T, key?: any): Promise { + put( + store: S, + value: StoreContent, + key?: any, + ): Promise { const req = this.tx.objectStore(store.name).put(value, key); return requestToPromise(req); } - add(store: Store, value: T, key?: any): Promise { + add( + store: S, + value: StoreContent, + key?: any, + ): Promise { const req = this.tx.objectStore(store.name).add(value, key); return requestToPromise(req); } - get(store: Store, key: any): Promise { + get( + store: S, + key: any, + ): Promise | undefined> { const req = this.tx.objectStore(store.name).get(key); return requestToPromise(req); } - getIndexed( - index: Index, + getIndexed< + StoreName extends StrKey, + IndexName extends string, + S extends IDBValidKey, + T + >( + index: Index, key: any, ): Promise { const req = this.tx @@ -302,15 +321,20 @@ export class TransactionHandle { return requestToPromise(req); } - iter(store: Store, key?: any): ResultStream { + iter, T extends StoreTypes[N]>( + store: Store, + key?: any, + ): ResultStream { const req = this.tx.objectStore(store.name).openCursor(key); return new ResultStream(req); } - iterIndexed( - index: Index, - key?: any, - ): ResultStream { + iterIndexed< + StoreName extends StrKey, + IndexName extends string, + S extends IDBValidKey, + T + >(index: Index, key?: any): ResultStream { const req = this.tx .objectStore(index.storeName) .index(index.indexName) @@ -318,13 +342,16 @@ export class TransactionHandle { return new ResultStream(req); } - delete(store: Store, key: any): Promise { + delete, T extends StoreTypes[N]>( + store: Store, + key: any, + ): Promise { const req = this.tx.objectStore(store.name).delete(key); return requestToPromise(req); } - mutate( - store: Store, + mutate, T extends StoreTypes[N]>( + store: Store, key: any, f: (x: T) => T | undefined, ): Promise { @@ -333,10 +360,10 @@ export class TransactionHandle { } } -function runWithTransaction( +function runWithTransaction>( db: IDBDatabase, - stores: Store[], - f: (t: TransactionHandle) => Promise, + stores: StoreTypes[], + f: (t: TransactionHandle) => Promise, mode: "readonly" | "readwrite", ): Promise { const stack = Error("Failed transaction was started here."); @@ -397,7 +424,12 @@ function runWithTransaction( /** * Definition of an index. */ -export class Index { +export class Index< + StoreName extends string, + IndexName extends string, + S extends IDBValidKey, + T +> { /** * Name of the store that this index is associated with. */ @@ -409,8 +441,8 @@ export class Index { options: IndexOptions; constructor( - s: Store, - public indexName: string, + s: Store, + public indexName: IndexName, public keyPath: string | string[], options?: IndexOptions, ) { @@ -539,7 +571,10 @@ export class Database { }); } - async get(store: Store, key: any): Promise { + async get( + store: Store, + key: any, + ): Promise { const tx = this.db.transaction([store.name], "readonly"); const req = tx.objectStore(store.name).get(key); const v = await requestToPromise(req); @@ -547,10 +582,12 @@ export class Database { return v; } - async getIndexed( - index: Index, + async getIndexed>( + index: Ind extends Index + ? Index + : never, key: any, - ): Promise { + ): Promise | undefined> { const tx = this.db.transaction([index.storeName], "readonly"); const req = tx.objectStore(index.storeName).index(index.indexName).get(key); const v = await requestToPromise(req); @@ -558,7 +595,11 @@ export class Database { return v; } - async put(store: Store, value: T, key?: any): Promise { + async put>( + store: St extends Store ? Store : never, + value: St extends Store ? R : never, + key?: any, + ): Promise { const tx = this.db.transaction([store.name], "readwrite"); const req = tx.objectStore(store.name).put(value, key); const v = await requestToPromise(req); @@ -566,8 +607,8 @@ export class Database { return v; } - async mutate( - store: Store, + async mutate( + store: Store, key: any, f: (x: T) => T | undefined, ): Promise { @@ -577,14 +618,14 @@ export class Database { await transactionToPromise(tx); } - iter(store: Store): ResultStream { + iter(store: Store): ResultStream { const tx = this.db.transaction([store.name], "readonly"); const req = tx.objectStore(store.name).openCursor(); return new ResultStream(req); } - iterIndex( - index: Index, + iterIndex( + index: Index, query?: any, ): ResultStream { const tx = this.db.transaction([index.storeName], "readonly"); @@ -595,17 +636,17 @@ export class Database { return new ResultStream(req); } - async runWithReadTransaction( - stores: Store[], - f: (t: TransactionHandle) => Promise, + async runWithReadTransaction>( + stores: StoreTypes[], + f: (t: TransactionHandle) => Promise, ): Promise { - return runWithTransaction(this.db, stores, f, "readonly"); + return runWithTransaction(this.db, stores, f, "readonly"); } - async runWithWriteTransaction( - stores: Store[], - f: (t: TransactionHandle) => Promise, + async runWithWriteTransaction>( + stores: StoreTypes[], + f: (t: TransactionHandle) => Promise, ): Promise { - return runWithTransaction(this.db, stores, f, "readwrite"); + return runWithTransaction(this.db, stores, f, "readwrite"); } } -- cgit v1.2.3