taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 5fc116155854d334d131db06e2e8db6f6add855b
parent 40d4492e1b52bc77b1d06e27cfe2083d03e4f743
Author: Florian Dold <dold@taler.net>
Date:   Sun,  9 Aug 2026 14:39:28 +0200

idb-bridge: enrich error reporting

Diffstat:
Mpackages/idb-bridge/src/SqliteBackend.ts | 33++++++++++++++++++++++++++++-----
Mpackages/idb-bridge/src/bridge-idb.ts | 291+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Apackages/idb-bridge/src/error-reporting.test.ts | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/idb-bridge/src/util/makeStoreKeyValue.ts | 8++++++--
Mpackages/idb-bridge/src/util/valueToKey.ts | 18+++++++++++++-----
5 files changed, 340 insertions(+), 84 deletions(-)

diff --git a/packages/idb-bridge/src/SqliteBackend.ts b/packages/idb-bridge/src/SqliteBackend.ts @@ -1726,7 +1726,12 @@ export class SqliteBackend implements Backend { const value = structuredRevive(JSON.parse(keyRow.value)); assertDbInvariant(key instanceof Uint8Array); try { - await this.insertIntoIndex(indexMeta, key, value); + await this.insertIntoIndex( + storeMeta.currentName ?? "<unknown>", + indexMeta, + key, + value, + ); } catch (e) { // FIXME: Catch this in insertIntoIndex! if (e instanceof DataError) { @@ -1941,7 +1946,11 @@ export class SqliteBackend implements Backend { } else { if (keyPath != null && storeReq.key !== undefined) { // If in-line keys are used, a key can't be explicitly specified. - throw new DataError(); + throw new DataError( + `Cannot store a record in object store ${JSON.stringify( + storeReq.objectStoreName, + )}: an explicit key cannot be provided with in-line key path ${JSON.stringify(keyPath)}.`, + ); } const storeKeyResult = makeStoreKeyValue({ @@ -1969,7 +1978,11 @@ export class SqliteBackend implements Backend { if (storeReq.storeLevel === StoreLevel.NoOverwrite) { if (existingObj) { - throw new ConstraintError(); + throw new ConstraintError( + `Cannot add a record to object store ${JSON.stringify( + storeReq.objectStoreName, + )}: a record with the same primary key already exists.`, + ); } } @@ -2001,7 +2014,12 @@ export class SqliteBackend implements Backend { } try { - await this.insertIntoIndex(indexInfo, serializedObjectKey, value); + await this.insertIntoIndex( + storeReq.objectStoreName, + indexInfo, + serializedObjectKey, + value, + ); } catch (e) { // FIXME: handle this in insertIntoIndex! if (e instanceof DataError) { @@ -2040,6 +2058,7 @@ export class SqliteBackend implements Backend { } private async insertIntoIndex( + objectStoreName: string, indexInfo: MyIndexMeta, primaryKey: Uint8Array, value: any, @@ -2071,7 +2090,11 @@ export class SqliteBackend implements Backend { }); } catch (e: any) { if (e.code === SqliteError.constraintPrimarykey) { - throw new ConstraintError(); + throw new ConstraintError( + `Unique index ${JSON.stringify(indexInfo.currentName)} on object store ${JSON.stringify( + objectStoreName, + )} (key path ${JSON.stringify(indexInfo.keyPath)}) already contains this key.`, + ); } throw e; } diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts @@ -77,6 +77,7 @@ export type CursorSource = BridgeIDBIndex | BridgeIDBObjectStore; export interface RequestObj { operation: () => Promise<any>; + operationName: string; request?: BridgeIDBRequest | undefined; source?: any; } @@ -98,6 +99,68 @@ function simplifyRange( return BridgeIDBKeyRange.bound(r, r, false, false); } +function errorContext(operationName: string, source: unknown): string { + let objectStore: BridgeIDBObjectStore | undefined; + let index: BridgeIDBIndex | undefined; + + if (source instanceof BridgeIDBIndex) { + index = source; + objectStore = source._objectStore; + } else if (source instanceof BridgeIDBObjectStore) { + objectStore = source; + } else if (source instanceof BridgeIDBCursor) { + const cursorSource = source.source; + if (cursorSource instanceof BridgeIDBIndex) { + index = cursorSource; + objectStore = cursorSource._objectStore; + } else { + objectStore = cursorSource; + } + } + + const details = [`${operationName}() failed`]; + if (objectStore) { + details.push(`object store ${JSON.stringify(objectStore.name)}`); + if (objectStore._objectStoreMeta.keyPath !== null) { + details.push( + `object-store key path ${JSON.stringify(objectStore._objectStoreMeta.keyPath)}`, + ); + } + } + if (index) { + details.push(`index ${JSON.stringify(index.name)}`); + details.push(`index key path ${JSON.stringify(index.keyPath)}`); + } + return details.join(", "); +} + +function addErrorContext( + err: unknown, + operationName: string, + source: unknown, +): Error { + const context = errorContext(operationName, source); + if (err instanceof Error) { + if (!err.message.startsWith(`${context}:`)) { + err.message = `${context}: ${err.message}`; + } + return err; + } + return new Error(`${context}: ${String(err)}`); +} + +function withErrorContext<T>( + operationName: string, + source: unknown, + operation: () => T, +): T { + try { + return operation(); + } catch (err) { + throw addErrorContext(err, operationName, source); + } +} + /** * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor */ @@ -324,23 +387,31 @@ export class BridgeIDBCursor implements IDBCursor { } try { - // Only called for the side effect of throwing an exception - checkStructuredCloneOrThrow(value); - } catch (e) { - throw new DataCloneError(); - } + try { + // Only called for the side effect of throwing an exception + checkStructuredCloneOrThrow(value); + } catch (e) { + throw new DataCloneError(); + } - if (os._objectStoreMeta.keyPath !== null) { - const key2 = extractKey(os._objectStoreMeta.keyPath, value); - if (compareKeys(key, key2) !== 0) { - throw new DataError(); + if (os._objectStoreMeta.keyPath !== null) { + const key2 = extractKey(os._objectStoreMeta.keyPath, value); + if (compareKeys(key, key2) !== 0) { + throw new DataError( + "The value's in-line primary key differs from the cursor's primary key.", + ); + } } - } - if (os.keyPath !== null && os.keyPath !== undefined) { - if (!canInjectKey(os.keyPath, value)) { - throw new DataError(); + if (os.keyPath !== null && os.keyPath !== undefined) { + if (!canInjectKey(os.keyPath, value)) { + throw new DataError( + "The value cannot accept a key at the object store's in-line key path.", + ); + } } + } catch (err) { + throw addErrorContext(err, "IDBCursor.update", this); } const storeReq: RecordStoreRequest = { @@ -360,6 +431,7 @@ export class BridgeIDBCursor implements IDBCursor { }; return transaction._execRequestAsync({ operation, + operationName: "IDBCursor.update", source: this, }); } @@ -406,6 +478,7 @@ export class BridgeIDBCursor implements IDBCursor { transaction._execRequestAsync({ operation, + operationName: "IDBCursor.advance", request: this._request, source: this.source, }); @@ -438,22 +511,27 @@ export class BridgeIDBCursor implements IDBCursor { } if (key !== undefined) { - key = valueToKey(key); - let lastKey = - this._indexName === undefined - ? this._objectStorePosition - : this._indexPosition; - - const cmpResult = compareKeys(key, lastKey); - - if ( - (cmpResult <= 0 && - (this.direction === "next" || this.direction === "nextunique")) || - (cmpResult >= 0 && - (this.direction === "prev" || this.direction === "prevunique")) - ) { - throw new DataError(); - } + key = withErrorContext("IDBCursor.continue", this, () => { + key = valueToKey(key); + const lastKey = + this._indexName === undefined + ? this._objectStorePosition + : this._indexPosition; + + const cmpResult = compareKeys(key, lastKey); + + if ( + (cmpResult <= 0 && + (this.direction === "next" || this.direction === "nextunique")) || + (cmpResult >= 0 && + (this.direction === "prev" || this.direction === "prevunique")) + ) { + throw new DataError( + `The continuation key must be after the current cursor key for ${this.direction} cursors.`, + ); + } + return key; + }); } if (this._request) { @@ -466,6 +544,7 @@ export class BridgeIDBCursor implements IDBCursor { transaction._execRequestAsync({ operation, + operationName: "IDBCursor.continue", request: this._request, source: this.source, }); @@ -514,6 +593,7 @@ export class BridgeIDBCursor implements IDBCursor { return transaction._execRequestAsync({ operation, + operationName: "IDBCursor.delete", source: this, }); } @@ -682,7 +762,11 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase { if (this._objectStoreSet.includes(name)) { // Already exists - throw new ConstraintError(); + throw new ConstraintError( + `IDBDatabase.createObjectStore() failed for object store ${JSON.stringify( + name, + )}: it already exists.`, + ); } if (autoIncrement && (keyPath === "" || Array.isArray(keyPath))) { @@ -1230,7 +1314,9 @@ export class BridgeIDBIndex implements IDBIndex { this._confirmActiveTransaction(); - range = simplifyRange(range); + range = withErrorContext("IDBIndex.openCursor", this, () => + simplifyRange(range), + ); const request = new BridgeIDBRequest(); request._source = this; @@ -1251,6 +1337,7 @@ export class BridgeIDBIndex implements IDBIndex { return this._objectStore._transaction._execRequestAsync({ operation, + operationName: "IDBIndex.openCursor", request, source: this, }); @@ -1268,7 +1355,9 @@ export class BridgeIDBIndex implements IDBIndex { range = undefined; } if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) { - range = BridgeIDBKeyRange.only(valueToKey(range)); + range = withErrorContext("IDBIndex.openKeyCursor", this, () => + BridgeIDBKeyRange.only(valueToKey(range)), + ); } const request = new BridgeIDBRequest(); @@ -1287,6 +1376,7 @@ export class BridgeIDBIndex implements IDBIndex { return this._objectStore._transaction._execRequestAsync({ operation: cursor._iterate.bind(cursor), + operationName: "IDBIndex.openKeyCursor", request, source: this, }); @@ -1306,7 +1396,9 @@ export class BridgeIDBIndex implements IDBIndex { this._confirmActiveTransaction(); if (!(key instanceof BridgeIDBKeyRange)) { - key = BridgeIDBKeyRange._valueToKeyRange(key); + key = withErrorContext("IDBIndex.get", this, () => + BridgeIDBKeyRange._valueToKeyRange(key), + ); } const getReq: IndexGetQuery = { @@ -1333,6 +1425,7 @@ export class BridgeIDBIndex implements IDBIndex { return this._objectStore._transaction._execRequestAsync({ operation, + operationName: "IDBIndex.get", source: this, }); } @@ -1349,7 +1442,9 @@ export class BridgeIDBIndex implements IDBIndex { } if (!(query instanceof BridgeIDBKeyRange)) { - query = BridgeIDBKeyRange._valueToKeyRange(query); + query = withErrorContext("IDBIndex.getAll", this, () => + BridgeIDBKeyRange._valueToKeyRange(query), + ); } if (count === undefined) { @@ -1377,6 +1472,7 @@ export class BridgeIDBIndex implements IDBIndex { return this._objectStore._transaction._execRequestAsync({ operation, + operationName: "IDBIndex.getAll", source: this, }); } @@ -1387,7 +1483,9 @@ export class BridgeIDBIndex implements IDBIndex { this._confirmActiveTransaction(); if (!(key instanceof BridgeIDBKeyRange)) { - key = BridgeIDBKeyRange._valueToKeyRange(key); + key = withErrorContext("IDBIndex.getKey", this, () => + BridgeIDBKeyRange._valueToKeyRange(key), + ); } const getReq: IndexGetQuery = { @@ -1414,6 +1512,7 @@ export class BridgeIDBIndex implements IDBIndex { return this._objectStore._transaction._execRequestAsync({ operation, + operationName: "IDBIndex.getKey", source: this, }); } @@ -1427,7 +1526,9 @@ export class BridgeIDBIndex implements IDBIndex { this._confirmActiveTransaction(); if (!(query instanceof BridgeIDBKeyRange)) { - query = BridgeIDBKeyRange._valueToKeyRange(query); + query = withErrorContext("IDBIndex.getAllKeys", this, () => + BridgeIDBKeyRange._valueToKeyRange(query), + ); } if (count === undefined) { @@ -1455,6 +1556,7 @@ export class BridgeIDBIndex implements IDBIndex { return this._objectStore._transaction._execRequestAsync({ operation, + operationName: "IDBIndex.getAllKeys", source: this, }); } @@ -1468,7 +1570,9 @@ export class BridgeIDBIndex implements IDBIndex { key = undefined; } if (key !== undefined && !(key instanceof BridgeIDBKeyRange)) { - key = BridgeIDBKeyRange.only(valueToKey(key)); + key = withErrorContext("IDBIndex.count", this, () => + BridgeIDBKeyRange.only(valueToKey(key)), + ); } const getReq: IndexGetQuery = { @@ -1490,6 +1594,7 @@ export class BridgeIDBIndex implements IDBIndex { return this._objectStore._transaction._execRequestAsync({ operation, + operationName: "IDBIndex.count", source: this, }); } @@ -1776,20 +1881,27 @@ export class BridgeIDBObjectStore implements IDBObjectStore { } const { keyPath, autoIncrement } = this._objectStoreMeta; + const operationName = overwrite + ? "IDBObjectStore.put" + : "IDBObjectStore.add"; - if (key !== null && key !== undefined) { - valueToKey(key); - } + try { + if (key !== null && key !== undefined) { + valueToKey(key); + } - // We only call this to synchronously verify the request. - // FIXME: The backend should do that! - makeStoreKeyValue({ - value: value, - key: key, - currentKeyGenerator: 1, - autoIncrement, - keyPath, - }); + // We only call this to synchronously verify the request. + // FIXME: The backend should do that! + makeStoreKeyValue({ + value: value, + key: key, + currentKeyGenerator: 1, + autoIncrement, + keyPath, + }); + } catch (err) { + throw addErrorContext(err, operationName, this); + } const operation = async () => { const { btx } = this._confirmStartedBackendTransaction(); @@ -1804,7 +1916,11 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return result.key; }; - return this._transaction._execRequestAsync({ operation, source: this }); + return this._transaction._execRequestAsync({ + operation, + operationName, + source: this, + }); } public put(value: any, key?: IDBValidKey) { @@ -1842,7 +1958,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (key instanceof BridgeIDBKeyRange) { keyRange = key; } else { - keyRange = BridgeIDBKeyRange.only(valueToKey(key)); + keyRange = withErrorContext("IDBObjectStore.delete", this, () => + BridgeIDBKeyRange.only(valueToKey(key)), + ); } const operation = async () => { @@ -1852,6 +1970,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._execRequestAsync({ operation, + operationName: "IDBObjectStore.delete", source: this, }); } @@ -1880,13 +1999,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (key instanceof BridgeIDBKeyRange) { keyRange = key; } else { - try { - keyRange = BridgeIDBKeyRange.only(valueToKey(key)); - } catch (e) { - throw new DataError( - `invalid key (type ${typeof key}) for object store '${this._name}'`, - ); - } + keyRange = withErrorContext("IDBObjectStore.get", this, () => + BridgeIDBKeyRange.only(valueToKey(key)), + ); } const recordRequest: ObjectStoreGetQuery = { @@ -1927,6 +2042,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._execRequestAsync({ operation, + operationName: "IDBObjectStore.get", source: this, }); } @@ -1954,7 +2070,11 @@ export class BridgeIDBObjectStore implements IDBObjectStore { count = -1; } - let keyRange: BridgeIDBKeyRange | null = simplifyRange(query); + let keyRange: BridgeIDBKeyRange | null = withErrorContext( + "IDBObjectStore.getAll", + this, + () => simplifyRange(query), + ); const recordRequest: ObjectStoreGetQuery = { objectStoreName: this._name, @@ -1987,6 +2107,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._execRequestAsync({ operation, + operationName: "IDBObjectStore.getAll", source: this, }); } @@ -2009,7 +2130,11 @@ export class BridgeIDBObjectStore implements IDBObjectStore { ); } - let keyRange: BridgeIDBKeyRange | null = simplifyRange(query); + let keyRange: BridgeIDBKeyRange | null = withErrorContext( + "IDBObjectStore.getKey", + this, + () => simplifyRange(query), + ); const recordRequest: ObjectStoreGetQuery = { objectStoreName: this._name, @@ -2049,6 +2174,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._execRequestAsync({ operation, + operationName: "IDBObjectStore.getKey", source: this, }); } @@ -2081,13 +2207,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (query instanceof BridgeIDBKeyRange) { keyRange = query; } else { - try { - keyRange = BridgeIDBKeyRange.only(valueToKey(query)); - } catch (e) { - throw new DataError( - `invalid key (type ${typeof query}) for object store '${this._name}'`, - ); - } + keyRange = withErrorContext("IDBObjectStore.getAllKeys", this, () => + BridgeIDBKeyRange.only(valueToKey(query)), + ); } const recordRequest: ObjectStoreGetQuery = { @@ -2115,6 +2237,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._execRequestAsync({ operation, + operationName: "IDBObjectStore.getAllKeys", source: this, }); } @@ -2137,6 +2260,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._execRequestAsync({ operation, + operationName: "IDBObjectStore.clear", source: this, }); } @@ -2154,7 +2278,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { range = undefined; } if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) { - range = BridgeIDBKeyRange.only(valueToKey(range)); + range = withErrorContext("IDBObjectStore.openCursor", this, () => + BridgeIDBKeyRange.only(valueToKey(range)), + ); } const request = new BridgeIDBRequest(); @@ -2172,6 +2298,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._execRequestAsync({ operation: () => cursor._iterate(), + operationName: "IDBObjectStore.openCursor", request, source: this, }); @@ -2190,7 +2317,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { range = undefined; } if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) { - range = BridgeIDBKeyRange.only(valueToKey(range)); + range = withErrorContext("IDBObjectStore.openKeyCursor", this, () => + BridgeIDBKeyRange.only(valueToKey(range)), + ); } if (!direction) { @@ -2213,6 +2342,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._execRequestAsync({ operation: cursor._iterate.bind(cursor), + operationName: "IDBObjectStore.openKeyCursor", request, source: this, }); @@ -2248,7 +2378,13 @@ export class BridgeIDBObjectStore implements IDBObjectStore { } if (this._objectStoreMeta.indexSet.indexOf(indexName) >= 0) { - throw new ConstraintError(); + throw new ConstraintError( + `IDBObjectStore.createIndex() failed for object store ${JSON.stringify( + this._name, + )}, index ${JSON.stringify(indexName)} (key path ${JSON.stringify( + keyPath, + )}): the index already exists.`, + ); } validateKeyPath(keyPath); @@ -2336,7 +2472,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { key = undefined; } if (key !== undefined && !(key instanceof BridgeIDBKeyRange)) { - key = BridgeIDBKeyRange.only(valueToKey(key)); + key = withErrorContext("IDBObjectStore.count", this, () => + BridgeIDBKeyRange.only(valueToKey(key)), + ); } const recordGetRequest: ObjectStoreGetQuery = { @@ -2357,7 +2495,11 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return result.count; }; - return this._transaction._execRequestAsync({ operation, source: this }); + return this._transaction._execRequestAsync({ + operation, + operationName: "IDBObjectStore.count", + source: this, + }); } public toString() { @@ -2551,6 +2693,7 @@ export class BridgeIDBTransaction public _scope: Set<string>; private _requests: Array<{ operation: () => Promise<void>; + operationName: string; request: BridgeIDBRequest; }> = []; @@ -2740,6 +2883,7 @@ export class BridgeIDBTransaction this._requests.push({ operation, + operationName: obj.operationName, request, }); @@ -2761,6 +2905,7 @@ export class BridgeIDBTransaction // Remove from request queue - cursor ones will be added back if necessary // by cursor.continue and such let operation; + let operationName; let request; while (this._requests.length > 0) { const r = this._requests.shift(); @@ -2769,6 +2914,7 @@ export class BridgeIDBTransaction if (r && r.request.readyState !== "done") { request = r.request; operation = r.operation; + operationName = r.operationName; break; } } @@ -2860,6 +3006,7 @@ export class BridgeIDBTransaction if (BridgeIDBFactory.enableTracing) { console.log("TRACING: error during operation: ", err); } + err = addErrorContext(err, operationName!, request._source); request.readyState = "done"; request.result = undefined; request.error = err; diff --git a/packages/idb-bridge/src/error-reporting.test.ts b/packages/idb-bridge/src/error-reporting.test.ts @@ -0,0 +1,74 @@ +import assert from "node:assert"; +import { before, test } from "node:test"; +import { BridgeIDBDatabase } from "./bridge-idb.js"; +import { promiseFromRequest, promiseFromTransaction } from "./idbpromutil.js"; +import { initTestIndexedDB, useTestIndexedDb } from "./testingdb.js"; + +before(initTestIndexedDB); + +async function createDatabase(): Promise<BridgeIDBDatabase> { + const request = useTestIndexedDb().open( + `error-reporting-${Date.now()}-${Math.random()}`, + ); + request.onupgradeneeded = () => { + const db = request.result as BridgeIDBDatabase; + const store = db.createObjectStore("records", { keyPath: "id" }); + store.createIndex("by-email", "email", { unique: true }); + }; + return promiseFromRequest(request); +} + +test("store validation errors name the operation, store, and key path", async () => { + const db = await createDatabase(); + const tx = db.transaction("records", "readwrite"); + const store = tx.objectStore("records"); + + assert.throws( + () => store.add({ id: 1 }, 1), + (err: Error) => + err.name === "DataError" && + err.message.includes("IDBObjectStore.add() failed") && + err.message.includes('object store "records"') && + err.message.includes('object-store key path "id"') && + err.message.includes("explicit key cannot be provided"), + ); +}); + +test("index key errors name the operation, store, index, and key paths", async () => { + const db = await createDatabase(); + const tx = db.transaction("records", "readonly"); + const index = tx.objectStore("records").index("by-email"); + + assert.throws( + () => index.get(NaN), + (err: Error) => + err.name === "DataError" && + err.message.includes("IDBIndex.get() failed") && + err.message.includes('object store "records"') && + err.message.includes('index "by-email"') && + err.message.includes('index key path "email"') && + err.message.includes("NaN is not a valid key"), + ); +}); + +test("unique-index request errors identify the conflicting index", async () => { + const db = await createDatabase(); + const seedTx = db.transaction("records", "readwrite"); + seedTx.objectStore("records").add({ id: 1, email: "first@example.com" }); + await promiseFromTransaction(seedTx); + + const tx = db.transaction("records", "readwrite"); + const request = tx + .objectStore("records") + .add({ id: 2, email: "first@example.com" }); + + await assert.rejects( + promiseFromRequest(request), + (err: Error) => + err.name === "ConstraintError" && + err.message.includes("IDBObjectStore.add() failed") && + err.message.includes('object store "records"') && + err.message.includes('Unique index "by-email"') && + err.message.includes('key path "email"'), + ); +}); diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts @@ -103,7 +103,9 @@ export function makeStoreKeyValue(req: MakeStoreKvRequest): StoreKeyResult { if (haveKeyPath) { // (yes, yes, no) // (yes, yes, yes) - throw new DataError(); + throw new DataError( + "An explicit key cannot be provided when the object store uses an in-line key path.", + ); } else { if (autoIncrement) { // (yes, no, yes) @@ -175,7 +177,9 @@ export function makeStoreKeyValue(req: MakeStoreKvRequest): StoreKeyResult { }; } else { // (no, no, no) - throw new DataError(); + throw new DataError( + "A key is required because the object store has neither a key path nor a key generator.", + ); } } } diff --git a/packages/idb-bridge/src/util/valueToKey.ts b/packages/idb-bridge/src/util/valueToKey.ts @@ -28,13 +28,15 @@ export function valueToKey( ): IDBValidKey | IDBValidKey[] { if (typeof input === "number") { if (isNaN(input)) { - throw new DataError(); + throw new DataError("Invalid IndexedDB key: NaN is not a valid key."); } return input; } else if (input instanceof Date) { const ms = input.valueOf(); if (isNaN(ms)) { - throw new DataError(); + throw new DataError( + "Invalid IndexedDB key: an invalid Date is not a valid key.", + ); } return new Date(ms); } else if (typeof input === "string") { @@ -53,7 +55,9 @@ export function valueToKey( if (seen === undefined) { seen = new Set(); } else if (seen.has(input)) { - throw new DataError(); + throw new DataError( + "Invalid IndexedDB key: key arrays must not be circular.", + ); } seen.add(input); @@ -61,7 +65,9 @@ export function valueToKey( for (let i = 0; i < input.length; i++) { const hop = input.hasOwnProperty(i); if (!hop) { - throw new DataError(); + throw new DataError( + "Invalid IndexedDB key: key arrays must not be sparse.", + ); } const entry = input[i]; const key = valueToKey(entry, seen); @@ -69,6 +75,8 @@ export function valueToKey( } return keys; } else { - throw new DataError(); + throw new DataError( + `Invalid IndexedDB key: values of type ${typeof input} are not valid keys.`, + ); } }