fix tip record creation, migrate DB

This commit is contained in:
Florian Dold 2020-11-16 14:12:37 +01:00
parent cdf5cc583c
commit 292160f7e9
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 89 additions and 29 deletions

View File

@ -1,6 +1,7 @@
import { Stores } from "./types/dbTypes"; import { Stores } from "./types/dbTypes";
import { openDatabase, Database, Store, Index } from "./util/query"; import { openDatabase, Database, Store, Index } from "./util/query";
import { IDBFactory, IDBDatabase } from "idb-bridge"; import { IDBFactory, IDBDatabase, IDBObjectStore, IDBTransaction } from "idb-bridge";
import { Logger } from './util/logging';
/** /**
* Name of the Taler database. This is effectively the major * Name of the Taler database. This is effectively the major
@ -17,7 +18,9 @@ const TALER_DB_NAME = "taler-wallet-prod-v1";
* backwards-compatible way or object stores and indices * backwards-compatible way or object stores and indices
* are added. * are added.
*/ */
export const WALLET_DB_MINOR_VERSION = 1; export const WALLET_DB_MINOR_VERSION = 2;
const logger = new Logger("db.ts");
/** /**
* Return a promise that resolves * Return a promise that resolves
@ -31,9 +34,9 @@ export function openTalerDatabase(
db: IDBDatabase, db: IDBDatabase,
oldVersion: number, oldVersion: number,
newVersion: number, newVersion: number,
upgradeTransaction: IDBTransaction,
): void => { ): void => {
switch (oldVersion) { if (oldVersion === 0) {
case 0: // DB does not exist yet
for (const n in Stores) { for (const n in Stores) {
if ((Stores as any)[n] instanceof Store) { if ((Stores as any)[n] instanceof Store) {
const si: Store<any> = (Stores as any)[n]; const si: Store<any> = (Stores as any)[n];
@ -46,9 +49,30 @@ export function openTalerDatabase(
} }
} }
} }
break; return;
default: }
throw Error("unsupported existig DB version"); if (oldVersion === newVersion) {
return;
}
logger.info(`upgrading database from ${oldVersion} to ${newVersion}`);
for (const n in Stores) {
if ((Stores as any)[n] instanceof Store) {
const si: Store<any> = (Stores as any)[n];
let s: IDBObjectStore;
if ((si.storeParams?.versionAdded ?? 1) > oldVersion) {
s = db.createObjectStore(si.name, si.storeParams);
} else {
s = upgradeTransaction.objectStore(si.name);
}
for (const indexName in si as any) {
if ((si as any)[indexName] instanceof Index) {
const ii: Index<any, any> = (si as any)[indexName];
if ((ii.options?.versionAdded ?? 0) > oldVersion) {
s.createIndex(ii.indexName, ii.keyPath, ii.options);
}
}
}
}
} }
}; };

View File

@ -46,6 +46,7 @@ import { Logger } from "../util/logging";
import { checkDbInvariant } from "../util/invariants"; import { checkDbInvariant } from "../util/invariants";
import { TalerErrorCode } from "../TalerErrorCode"; import { TalerErrorCode } from "../TalerErrorCode";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
import { j2s } from '../util/helpers';
const logger = new Logger("operations/tip.ts"); const logger = new Logger("operations/tip.ts");
@ -68,7 +69,7 @@ export async function prepareTip(
merchantResp, merchantResp,
codecForTipPickupGetResponse(), codecForTipPickupGetResponse(),
); );
logger.trace(`status ${tipPickupStatus}`); logger.trace(`status ${j2s(tipPickupStatus)}`);
const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount); const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount);
@ -80,6 +81,7 @@ export async function prepareTip(
]); ]);
if (!tipRecord) { if (!tipRecord) {
logger.trace("new tip, creating tip record");
await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url); await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
const withdrawDetails = await getExchangeWithdrawalInfo( const withdrawDetails = await getExchangeWithdrawalInfo(
ws, ws,
@ -232,7 +234,7 @@ async function processTipImpl(
})); }));
const tipStatusUrl = new URL( const tipStatusUrl = new URL(
`/tips/${tipRecord.merchantTipId}/pickup`, `tips/${tipRecord.merchantTipId}/pickup`,
tipRecord.merchantBaseUrl, tipRecord.merchantBaseUrl,
); );

View File

@ -361,7 +361,6 @@ export enum DenominationStatus {
VerifiedBad, VerifiedBad,
} }
/** /**
* Denomination record as stored in the wallet's database. * Denomination record as stored in the wallet's database.
*/ */
@ -1532,7 +1531,6 @@ export enum ImportPayloadType {
CoreSchema = "core-schema", CoreSchema = "core-schema",
} }
class ExchangesStore extends Store<ExchangeRecord> { class ExchangesStore extends Store<ExchangeRecord> {
constructor() { constructor() {
super("exchanges", { keyPath: "baseUrl" }); super("exchanges", { keyPath: "baseUrl" });
@ -1624,6 +1622,15 @@ class TipsStore extends Store<TipRecord> {
constructor() { constructor() {
super("tips", { keyPath: "walletTipId" }); super("tips", { keyPath: "walletTipId" });
} }
// Added in version 2
byMerchantTipIdAndBaseUrl = new Index<[string, string], TipRecord>(
this,
"tipsByMerchantTipIdAndOriginIndex",
["merchantTipId", "merchantBaseUrl"],
{
versionAdded: 2,
}
);
} }
class WithdrawalGroupsStore extends Store<WithdrawalGroupRecord> { class WithdrawalGroupsStore extends Store<WithdrawalGroupRecord> {

View File

@ -146,3 +146,7 @@ export function strcmp(s1: string, s2: string): number {
} }
return 0; return 0;
} }
export function j2s(x: any): string {
return JSON.stringify(x, undefined, 2);
}

View File

@ -44,14 +44,25 @@ const logger = new Logger("query.ts");
*/ */
export const TransactionAbort = Symbol("transaction_abort"); export const TransactionAbort = Symbol("transaction_abort");
export interface StoreParams<T> {
validator?: (v: T) => T;
autoIncrement?: boolean;
keyPath?: string | string[] | null;
/**
* Database version that this store was added in, or
* undefined if added in the first version.
*/
versionAdded?: number;
}
/** /**
* Definition of an object store. * Definition of an object store.
*/ */
export class Store<T> { export class Store<T> {
constructor( constructor(
public name: string, public name: string,
public storeParams?: IDBObjectStoreParameters, public storeParams?: StoreParams<T>,
public validator?: (v: T) => T,
) {} ) {}
} }
@ -66,6 +77,12 @@ export interface IndexOptions {
* Defaults to false. * Defaults to false.
*/ */
multiEntry?: boolean; multiEntry?: boolean;
/**
* Database version that this store was added in, or
* undefined if added in the first version.
*/
versionAdded?: number;
} }
function requestToPromise(req: IDBRequest): Promise<any> { function requestToPromise(req: IDBRequest): Promise<any> {
@ -425,6 +442,7 @@ export function openDatabase(
db: IDBDatabase, db: IDBDatabase,
oldVersion: number, oldVersion: number,
newVersion: number, newVersion: number,
upgradeTransaction: IDBTransaction,
) => void, ) => void,
): Promise<IDBDatabase> { ): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => { return new Promise<IDBDatabase>((resolve, reject) => {
@ -449,7 +467,11 @@ export function openDatabase(
if (!newVersion) { if (!newVersion) {
throw Error("upgrade needed, but new version unknown"); throw Error("upgrade needed, but new version unknown");
} }
onUpgradeNeeded(db, e.oldVersion, newVersion); const transaction = req.transaction;
if (!transaction) {
throw Error("no transaction handle available in upgrade handler");
}
onUpgradeNeeded(db, e.oldVersion, newVersion, transaction);
}; };
}); });
} }

View File

@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { canonicalizeBaseUrl } from './helpers';
import { URLSearchParams } from "./url"; import { URLSearchParams } from "./url";
export interface PayUriResult { export interface PayUriResult {
@ -59,7 +60,7 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
const p = [host, ...pathSegments].join("/"); const p = [host, ...pathSegments].join("/");
return { return {
bankIntegrationApiBaseUrl: `${pi.innerProto}://${p}/`, bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`),
withdrawalOperationId: withdrawId, withdrawalOperationId: withdrawId,
}; };
} }
@ -155,7 +156,7 @@ export function parsePayUri(s: string): PayUriResult | undefined {
const orderId = parts[parts.length - 2]; const orderId = parts[parts.length - 2];
const pathSegments = parts.slice(1, parts.length - 2); const pathSegments = parts.slice(1, parts.length - 2);
const p = [host, ...pathSegments].join("/"); const p = [host, ...pathSegments].join("/");
const merchantBaseUrl = `${pi.innerProto}://${p}/`; const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return { return {
merchantBaseUrl, merchantBaseUrl,
@ -183,7 +184,7 @@ export function parseTipUri(s: string): TipUriResult | undefined {
const tipId = parts[parts.length - 1]; const tipId = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1); const pathSegments = parts.slice(1, parts.length - 1);
const p = [host, ...pathSegments].join("/"); const p = [host, ...pathSegments].join("/");
const merchantBaseUrl = `${pi.innerProto}://${p}/`; const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return { return {
merchantBaseUrl, merchantBaseUrl,
@ -210,7 +211,7 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
const orderId = parts[parts.length - 2]; const orderId = parts[parts.length - 2];
const pathSegments = parts.slice(1, parts.length - 2); const pathSegments = parts.slice(1, parts.length - 2);
const p = [host, ...pathSegments].join("/"); const p = [host, ...pathSegments].join("/");
const merchantBaseUrl = `${pi.innerProto}://${p}/`; const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return { return {
merchantBaseUrl, merchantBaseUrl,

View File

@ -233,7 +233,7 @@ importers:
taler-wallet-core: 'workspace:*' taler-wallet-core: 'workspace:*'
tslib: ^2.0.1 tslib: ^2.0.1
typescript: ^3.9.7 typescript: ^3.9.7
lockfileVersion: 5.1 lockfileVersion: 5.2
packages: packages:
/@ava/typescript/1.1.1: /@ava/typescript/1.1.1:
dependencies: dependencies: