From 987f22de02648485ec6f1d3c1558abcfa6d624a0 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 16 Feb 2021 13:46:51 +0100 Subject: next batch of test cases and fixes --- packages/idb-bridge/src/MemoryBackend.ts | 58 ++++-- packages/idb-bridge/src/bridge-idb.ts | 75 ++++++- .../src/idb-wpt-ported/idbindex_get.test.ts | 177 +++++++++++++++++ .../src/idb-wpt-ported/idbobjectstore_add.test.ts | 218 ++++++++++++++++++++- .../idb-bridge/src/idb-wpt-ported/wptsupport.ts | 4 +- packages/idb-bridge/src/util/valueToKey.ts | 2 +- 6 files changed, 508 insertions(+), 26 deletions(-) create mode 100644 packages/idb-bridge/src/idb-wpt-ported/idbindex_get.test.ts (limited to 'packages/idb-bridge') diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts index 9a9527729..7107756a0 100644 --- a/packages/idb-bridge/src/MemoryBackend.ts +++ b/packages/idb-bridge/src/MemoryBackend.ts @@ -27,10 +27,7 @@ import { StoreLevel, RecordStoreResponse, } from "./backend-interface"; -import { - structuredClone, - structuredRevive, -} from "./util/structuredClone"; +import { structuredClone, structuredRevive } from "./util/structuredClone"; import { InvalidStateError, InvalidAccessError, @@ -42,11 +39,7 @@ import { compareKeys } from "./util/cmp"; import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue"; import { getIndexKeys } from "./util/getIndexKeys"; import { openPromise } from "./util/openPromise"; -import { - IDBKeyRange, - IDBTransactionMode, - IDBValidKey, -} from "./idbtypes"; +import { IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes"; import { BridgeIDBKeyRange } from "./bridge-idb"; type Key = IDBValidKey; @@ -488,10 +481,10 @@ export class MemoryBackend implements Backend { objectStores: string[], mode: IDBTransactionMode, ): Promise { + const transactionCookie = `tx-${this.transactionIdCounter++}`; if (this.enableTracing) { - console.log(`TRACING: beginTransaction`); + console.log(`TRACING: beginTransaction ${transactionCookie}`); } - const transactionCookie = `tx-${this.transactionIdCounter++}`; const myConn = this.connections[conn.connectionCookie]; if (!myConn) { throw Error("connection not found"); @@ -556,7 +549,7 @@ export class MemoryBackend implements Backend { async close(conn: DatabaseConnection): Promise { if (this.enableTracing) { - console.log(`TRACING: close`); + console.log(`TRACING: close (${conn.connectionCookie})`); } const myConn = this.connections[conn.connectionCookie]; if (!myConn) { @@ -640,7 +633,7 @@ export class MemoryBackend implements Backend { if (this.enableTracing) { console.log(`TRACING: deleteIndex(${indexName})`); } - const myConn = this.connections[btx.transactionCookie]; + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -670,9 +663,11 @@ export class MemoryBackend implements Backend { deleteObjectStore(btx: DatabaseTransaction, name: string): void { if (this.enableTracing) { - console.log(`TRACING: deleteObjectStore(${name})`); + console.log( + `TRACING: deleteObjectStore(${name}) in ${btx.transactionCookie}`, + ); } - const myConn = this.connections[btx.transactionCookie]; + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -714,7 +709,7 @@ export class MemoryBackend implements Backend { console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`); } - const myConn = this.connections[btx.transactionCookie]; + const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { throw Error("unknown connection"); } @@ -846,7 +841,15 @@ export class MemoryBackend implements Backend { objectStoreMapEntry.store.originalData; storeData.forEach((v, k) => { - this.insertIntoIndex(newIndex, k, v.value, indexProperties); + try { + this.insertIntoIndex(newIndex, k, v.value, indexProperties); + } catch (e) { + if (e instanceof DataError) { + // We don't propagate this error here. + return; + } + throw e; + } }); } @@ -1404,6 +1407,16 @@ export class MemoryBackend implements Backend { const autoIncrement = schema.objectStores[storeReq.objectStoreName].autoIncrement; const keyPath = schema.objectStores[storeReq.objectStoreName].keyPath; + + if ( + keyPath !== null && + keyPath !== undefined && + storeReq.key !== undefined + ) { + // If in-line keys are used, a key can't be explicitly specified. + throw new DataError(); + } + let storeKeyResult: StoreKeyResult; const revivedValue = structuredRevive(storeReq.value); try { @@ -1463,7 +1476,16 @@ export class MemoryBackend implements Backend { } const indexProperties = schema.objectStores[storeReq.objectStoreName].indexes[indexName]; - this.insertIntoIndex(index, key, value, indexProperties); + try { + this.insertIntoIndex(index, key, value, indexProperties); + } catch (e) { + if (e instanceof DataError) { + // https://www.w3.org/TR/IndexedDB-2/#object-store-storage-operation + // Do nothing + } else { + throw e; + } + } } return { key }; diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index f519b9c24..f518b4768 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -58,6 +58,7 @@ import { import { fakeDOMStringList } from "./util/fakeDOMStringList"; import FakeEvent from "./util/FakeEvent"; import FakeEventTarget from "./util/FakeEventTarget"; +import { makeStoreKeyValue } from "./util/makeStoreKeyValue"; import { normalizeKeyPath } from "./util/normalizeKeyPath"; import { openPromise } from "./util/openPromise"; import queueTask from "./util/queueTask"; @@ -605,7 +606,17 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase { throw new TypeError(); } const transaction = confirmActiveVersionchangeTransaction(this); - transaction._objectStoresCache.delete(name); + const backendTx = transaction._backendTransaction; + if (!backendTx) { + throw Error("invariant violated"); + } + this._backend.deleteObjectStore(backendTx, name); + const os = transaction._objectStoresCache.get(name); + if (os) { + os._deleted = true; + transaction._objectStoresCache.delete(name); + } + } public _internalTransaction( @@ -866,7 +877,9 @@ export class BridgeIDBFactory { event2.eventPath = [request]; request.dispatchEvent(event2); } else { - console.log(`dispatching success event, _active=${transaction._active}`); + console.log( + `dispatching success event, _active=${transaction._active}`, + ); const event2 = new FakeEvent("success", { bubbles: false, cancelable: false, @@ -1064,8 +1077,23 @@ export class BridgeIDBIndex implements IDBIndex { }); } - public get(key: BridgeIDBKeyRange | IDBValidKey) { + + private _confirmIndexExists() { + const storeSchema = this._schema.objectStores[this._objectStore._name]; + if (!storeSchema) { + throw new InvalidStateError(); + } + if (!storeSchema.indexes[this._name]) { + throw new InvalidStateError(); + } + } + + get(key: BridgeIDBKeyRange | IDBValidKey) { confirmActiveTransaction(this); + this._confirmIndexExists(); + if (this._deleted) { + throw new InvalidStateError(); + } if (!(key instanceof BridgeIDBKeyRange)) { key = BridgeIDBKeyRange._valueToKeyRange(key); @@ -1384,6 +1412,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { ); } + public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) { if (BridgeIDBFactory.enableTracing) { console.log(`TRACE: IDBObjectStore._store`); @@ -1391,6 +1420,22 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (this._transaction.mode === "readonly") { throw new ReadOnlyError(); } + + const { keyPath, autoIncrement } = this._schema.objectStores[this._name]; + + if (key !== null && key !== undefined) { + valueToKey(key); + } + + // We only call this to synchronously verify the request. + makeStoreKeyValue( + value, + key, + 1, + autoIncrement, + keyPath, + ); + const operation = async () => { const { btx } = this._confirmActiveTransaction(); const result = await this._backend.storeRecord(btx, { @@ -1411,6 +1456,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (arguments.length === 0) { throw new TypeError(); } + if (this._deleted) { + throw new InvalidStateError("tried to call 'put' on a deleted object store"); + } return this._store(value, key, true); } @@ -1418,6 +1466,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (arguments.length === 0) { throw new TypeError(); } + if (!this._schema.objectStores[this._name]) { + throw new InvalidStateError("object store does not exist"); + } return this._store(value, key, false); } @@ -1425,7 +1476,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (arguments.length === 0) { throw new TypeError(); } - + if (this._deleted) { + throw new InvalidStateError("tried to call 'delete' on a deleted object store"); + } if (this._transaction.mode === "readonly") { throw new ReadOnlyError(); } @@ -1458,6 +1511,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore { throw new TypeError(); } + if (this._deleted) { + throw new InvalidStateError("tried to call 'delete' on a deleted object store"); + } + let keyRange: BridgeIDBKeyRange; if (key instanceof BridgeIDBKeyRange) { @@ -1541,6 +1598,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { range?: IDBKeyRange | IDBValidKey, direction: IDBCursorDirection = "next", ) { + if (this._deleted) { + throw new InvalidStateError("tried to call 'openCursor' on a deleted object store"); + } if (range === null) { range = undefined; } @@ -1572,6 +1632,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { range?: BridgeIDBKeyRange | IDBValidKey, direction?: IDBCursorDirection, ) { + if (this._deleted) { + throw new InvalidStateError("tried to call 'openKeyCursor' on a deleted object store"); + } if (range === null) { range = undefined; } @@ -2091,7 +2154,9 @@ export class BridgeIDBTransaction request.dispatchEvent(event); } catch (err) { if (BridgeIDBFactory.enableTracing) { - console.log("TRACING: caught error in transaction success event handler"); + console.log( + "TRACING: caught error in transaction success event handler", + ); } this._abort("AbortError"); this._active = false; diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbindex_get.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbindex_get.test.ts new file mode 100644 index 000000000..8a8cb3129 --- /dev/null +++ b/packages/idb-bridge/src/idb-wpt-ported/idbindex_get.test.ts @@ -0,0 +1,177 @@ +import test from "ava"; +import { BridgeIDBKeyRange, BridgeIDBRequest } from ".."; +import { IDBDatabase } from "../idbtypes"; +import { createdb } from "./wptsupport"; + +// IDBIndex.get() - returns the record +test("WPT idbindex_get.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any, index: any; + const record = { key: 1, indexedProperty: "data" }; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + var objStore = db.createObjectStore("store", { keyPath: "key" }); + index = objStore.createIndex("index", "indexedProperty"); + + objStore.add(record); + }; + + open_rq.onsuccess = function (e) { + var rq = db + .transaction("store") + .objectStore("store") + .index("index") + .get(record.indexedProperty); + + rq.onsuccess = function (e: any) { + t.deepEqual(e.target.result.key, record.key); + resolve(); + }; + }; + }); + t.pass(); +}); + +// IDBIndex.get() - returns the record where the index contains duplicate values +test("WPT idbindex_get2.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + const records = [ + { key: 1, indexedProperty: "data" }, + { key: 2, indexedProperty: "data" }, + { key: 3, indexedProperty: "data" }, + ]; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + var objStore = db.createObjectStore("test", { keyPath: "key" }); + objStore.createIndex("index", "indexedProperty"); + + for (var i = 0; i < records.length; i++) objStore.add(records[i]); + }; + + open_rq.onsuccess = function (e) { + var rq = db + .transaction("test") + .objectStore("test") + .index("index") + .get("data"); + + rq.onsuccess = function (e: any) { + t.deepEqual(e.target.result.key, records[0].key); + resolve(); + }; + }; + }); + t.pass(); +}); + +// IDBIndex.get() - attempt to retrieve a record that doesn't exist +test("WPT idbindex_get3.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + var rq = db + .createObjectStore("test", { keyPath: "key" }) + .createIndex("index", "indexedProperty") + .get(1); + + rq.onsuccess = function (e: any) { + t.deepEqual(e.target.result, undefined); + resolve(); + }; + }; + }); + t.pass(); +}); + +// IDBIndex.get() - returns the record with the first key in the range +test("WPT idbindex_get4.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + + var open_rq = createdb(t); + + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + var store = db.createObjectStore("store", { keyPath: "key" }); + store.createIndex("index", "indexedProperty"); + + for (var i = 0; i < 10; i++) { + store.add({ key: i, indexedProperty: "data" + i }); + } + }; + + open_rq.onsuccess = function (e) { + var rq = db + .transaction("store") + .objectStore("store") + .index("index") + .get(BridgeIDBKeyRange.bound("data4", "data7")); + + rq.onsuccess = function (e: any) { + t.deepEqual(e.target.result.key, 4); + t.deepEqual(e.target.result.indexedProperty, "data4"); + setTimeout(function () { + resolve(); + }, 4); + }; + }; + }); + t.pass(); +}); + +// IDBIndex.get() - throw DataError when using invalid key +test("WPT idbindex_get5.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + + var index = db + .createObjectStore("test", { keyPath: "key" }) + .createIndex("index", "indexedProperty"); + t.throws( + function () { + index.get(NaN); + }, + { name: "DataError" }, + ); + resolve(); + }; + }); + t.pass(); +}); + +// IDBIndex.get() - throw InvalidStateError when the index is deleted +test("WPT idbindex_get6.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + var store = db.createObjectStore("store", { keyPath: "key" }); + var index = store.createIndex("index", "indexedProperty"); + + store.add({ key: 1, indexedProperty: "data" }); + store.deleteIndex("index"); + + t.throws( + function () { + index.get("data"); + }, + { name: "InvalidStateError" }, + ); + resolve(); + }; + }); + t.pass(); +}); diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore_add.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore_add.test.ts index b0c1de2d7..b8fdb5ac3 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore_add.test.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore_add.test.ts @@ -1,4 +1,5 @@ import test from "ava"; +import { BridgeIDBRequest } from ".."; import { IDBDatabase } from "../idbtypes"; import { createdb } from "./wptsupport"; @@ -228,7 +229,8 @@ test("WPT idbobjectstore_add7.htm", async (t) => { t.pass(); }); -// IDBObjectStore.add() - object store has autoIncrement:true and the key path is an object attribute +// IDBObjectStore.add() - object store has autoIncrement:true and the key path +// is an object attribute test("WPT idbobjectstore_add8.htm", async (t) => { await new Promise((resolve, reject) => { var db: any; @@ -268,3 +270,217 @@ test("WPT idbobjectstore_add8.htm", async (t) => { }); t.pass(); }); + +// IDBObjectStore.add() - Attempt to add a record that does not meet the +// constraints of an object store's inline key requirements +test("WPT idbobjectstore_add9.htm", async (t) => { + await new Promise((resolve, reject) => { + const record = { key: 1, property: "data" }; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + var rq, + db = e.target.result, + objStore = db.createObjectStore("store", { keyPath: "key" }); + + t.throws( + function () { + rq = objStore.add(record, 1); + }, + { name: "DataError" }, + ); + t.deepEqual(rq, undefined); + resolve(); + }; + }); + t.pass(); +}); + +// IDBObjectStore.add() - Attempt to call 'add' without an key parameter when the +// object store uses out-of-line keys. +test("WPT idbobjectstore_add10.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + const record = { property: "data" }; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + + var rq, + objStore = db.createObjectStore("store"); + + t.throws( + function () { + rq = objStore.add(record); + }, + { name: "DataError" }, + ); + + t.deepEqual(rq, undefined); + resolve(); + }; + }); + t.pass(); +}); + +// IDBObjectStore.add() - Attempt to add a record where the record's key +// does not meet the constraints of a valid key +test("WPT idbobjectstore_add11.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + const record = { key: { value: 1 }, property: "data" }; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + + var rq, + objStore = db.createObjectStore("store", { keyPath: "key" }); + + t.throws( + function () { + rq = objStore.add(record); + }, + { name: "DataError" }, + ); + + t.deepEqual(rq, undefined); + resolve(); + }; + }); + t.pass(); +}); + +// IDBObjectStore.add() - Attempt to add a record where the +// record's in-line key is not defined +test("WPT idbobjectstore_add12.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + const record = { property: "data" }; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + + var rq, + objStore = db.createObjectStore("store", { keyPath: "key" }); + t.throws( + function () { + rq = objStore.add(record); + }, + { name: "DataError" }, + ); + t.deepEqual(rq, undefined); + resolve(); + }; + }); + t.pass(); +}); + +// IDBObjectStore.add() - Attempt to add a record where the out of line +// key provided does not meet the constraints of a valid key +test("WPT idbobjectstore_add13.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + const record = { property: "data" }; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + + var rq, + objStore = db.createObjectStore("store"); + + t.throws( + function () { + rq = objStore.add(record, { value: 1 }); + }, + { name: "DataError" }, + ); + + t.deepEqual(rq, undefined); + resolve(); + }; + }); + t.pass(); +}); + +// IDBObjectStore.add() - Add a record where a value +// being indexed does not meet the constraints of a valid key +test("WPT idbobjectstore_add14.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + const record = { key: 1, indexedProperty: { property: "data" } }; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + + var rq, + objStore = db.createObjectStore("store", { keyPath: "key" }); + + objStore.createIndex("index", "indexedProperty"); + + rq = objStore.add(record); + + t.assert(rq instanceof BridgeIDBRequest); + rq.onsuccess = function () { + resolve(); + }; + }; + }); + t.pass(); +}); + +// IDBObjectStore.add() - If the transaction this IDBObjectStore belongs +// to has its mode set to readonly, throw ReadOnlyError +test("WPT idbobjectstore_add15.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (event: any) { + db = event.target.result; + db.createObjectStore("store", { keyPath: "pKey" }); + }; + + open_rq.onsuccess = function (event) { + var txn = db.transaction("store"); + var ostore = txn.objectStore("store"); + t.throws( + function () { + ostore.add({ pKey: "primaryKey_0" }); + }, + { name: "ReadOnlyError" }, + ); + resolve(); + }; + }); + t.pass(); +}); + +// IDBObjectStore.add() - If the object store has been +// deleted, the implementation must throw a DOMException of type InvalidStateError +test("WPT idbobjectstore_add16.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + let ostore: any; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (event: any) { + db = event.target.result; + ostore = db.createObjectStore("store", { keyPath: "pKey" }); + db.deleteObjectStore("store"); + + t.throws( + function () { + ostore.add({ pKey: "primaryKey_0" }); + }, + { name: "InvalidStateError" }, + ); + resolve(); + }; + }); + t.pass(); +}); diff --git a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts index 92e78a685..5716a7ae5 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts @@ -5,7 +5,9 @@ import { MemoryBackend } from "../MemoryBackend"; import { compareKeys } from "../util/cmp"; BridgeIDBFactory.enableTracing = true; -const idbFactory = new BridgeIDBFactory(new MemoryBackend()); +const backend = new MemoryBackend(); +backend.enableTracing = true; +const idbFactory = new BridgeIDBFactory(backend); const self = { indexedDB: idbFactory, diff --git a/packages/idb-bridge/src/util/valueToKey.ts b/packages/idb-bridge/src/util/valueToKey.ts index cfef779fe..c65604df1 100644 --- a/packages/idb-bridge/src/util/valueToKey.ts +++ b/packages/idb-bridge/src/util/valueToKey.ts @@ -17,7 +17,7 @@ import { IDBValidKey } from ".."; import { DataError } from "./errors"; -// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-input +// https://www.w3.org/TR/IndexedDB-2/#convert-a-value-to-a-key export function valueToKey( input: any, seen?: Set, -- cgit v1.2.3