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:
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.`,
+ );
}
}