A small wrapper that makes IndexedDB usable with promises and enhanced TypeScript support
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Enhanced index interface for querying data by secondary keys with full async iteration support.
Access index metadata and associated object store.
/**
* The IDBObjectStore the index belongs to
*/
readonly objectStore: IDBPObjectStore<DBTypes, TxStores, StoreName, Mode>;Usage Examples:
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
const emailIndex = userStore.index("email");
// Access parent object store
console.log("Parent store:", emailIndex.objectStore === userStore); // true
// Use parent store for operations not available on index
const user = await emailIndex.get("alice@example.com");
if (user) {
// Update user via parent store
const updateTx = db.transaction("users", "readwrite");
await updateTx.store.put({ ...user, lastLogin: new Date() });
await updateTx.done;
}Query records using index keys instead of primary keys.
/**
* Retrieves the value of the first record matching the query
* Resolves with undefined if no match is found
* @param query - Index key or key range to match
* @returns Promise resolving to the store value or undefined
*/
get(
query: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange
): Promise<StoreValue<DBTypes, StoreName> | undefined>;
/**
* Retrieves all values that match the query
* @param query - Index key or key range to match (optional)
* @param count - Maximum number of values to return (optional)
* @returns Promise resolving to array of matching store values
*/
getAll(
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
count?: number
): Promise<StoreValue<DBTypes, StoreName>[]>;/**
* Retrieves the key of the first record that matches the query
* Returns the primary key of the matching record
* Resolves with undefined if no match is found
* @param query - Index key or key range to match
* @returns Promise resolving to the primary store key or undefined
*/
getKey(
query: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange
): Promise<StoreKey<DBTypes, StoreName> | undefined>;
/**
* Retrieves the keys of records matching the query
* Returns primary keys of matching records
* @param query - Index key or key range to match (optional)
* @param count - Maximum number of keys to return (optional)
* @returns Promise resolving to array of primary store keys
*/
getAllKeys(
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
count?: number
): Promise<StoreKey<DBTypes, StoreName>[]>;/**
* Retrieves the number of records matching the given query
* @param key - Index key or key range to count (optional)
* @returns Promise resolving to the count
*/
count(
key?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null
): Promise<number>;Data Retrieval Examples:
interface UserDB {
users: {
key: number;
value: {
id: number;
name: string;
email: string;
age: number;
department: string;
tags: string[];
};
indexes: {
email: string;
name: string;
age: number;
department: string;
tags: string; // multiEntry index
};
};
}
const db = await openDB<UserDB>("company", 1, {
upgrade(db) {
const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true });
userStore.createIndex("email", "email", { unique: true });
userStore.createIndex("name", "name");
userStore.createIndex("age", "age");
userStore.createIndex("department", "department");
userStore.createIndex("tags", "tags", { multiEntry: true }); // Each tag creates separate index entry
}
});
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
// Query by email (unique index)
const emailIndex = userStore.index("email");
const userByEmail = await emailIndex.get("alice@company.com");
if (userByEmail) {
console.log("Found user by email:", userByEmail.name);
}
// Query by age range
const ageIndex = userStore.index("age");
const youngEmployees = await ageIndex.getAll(IDBKeyRange.upperBound(30));
const seniorEmployees = await ageIndex.getAll(IDBKeyRange.lowerBound(50));
// Query by department
const deptIndex = userStore.index("department");
const engineeringTeam = await deptIndex.getAll("Engineering");
const firstFiveInSales = await deptIndex.getAll("Sales", 5);
// Query by tags (multiEntry index)
const tagIndex = userStore.index("tags");
const jsExperts = await tagIndex.getAll("javascript");
const fullStackDevs = await tagIndex.getAll("full-stack");
// Get primary keys only (more efficient)
const engineeringIds = await deptIndex.getAllKeys("Engineering");
const seniorIds = await ageIndex.getAllKeys(IDBKeyRange.lowerBound(50));
// Count operations
const totalUsers = await userStore.count();
const engineeringCount = await deptIndex.count("Engineering");
const adultCount = await ageIndex.count(IDBKeyRange.lowerBound(18));
await tx.done;Open cursors for efficient iteration over index results.
/**
* Opens a cursor over the records matching the query
* Resolves with null if no matches are found
* @param query - Index key or key range to match (optional)
* @param direction - Cursor direction (next, nextunique, prev, prevunique)
* @returns Promise resolving to cursor with value or null
*/
openCursor(
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
direction?: IDBCursorDirection
): Promise<IDBPCursorWithValue<DBTypes, TxStores, StoreName, IndexName, Mode> | null>;
/**
* Opens a cursor over the keys matching the query
* Resolves with null if no matches are found
* @param query - Index key or key range to match (optional)
* @param direction - Cursor direction (next, nextunique, prev, prevunique)
* @returns Promise resolving to cursor or null
*/
openKeyCursor(
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
direction?: IDBCursorDirection
): Promise<IDBPCursor<DBTypes, TxStores, StoreName, IndexName, Mode> | null>;Cursor Usage Examples:
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
const ageIndex = userStore.index("age");
// Manual cursor iteration
let cursor = await ageIndex.openCursor(IDBKeyRange.bound(25, 35));
while (cursor) {
console.log(`User ${cursor.value.name} is ${cursor.key} years old`);
cursor = await cursor.continue();
}
// Cursor with unique values only
let uniqueCursor = await ageIndex.openCursor(null, "nextunique");
while (uniqueCursor) {
console.log("Unique age:", cursor.key);
uniqueCursor = await uniqueCursor.continue();
}
// Key cursor (more efficient when you don't need full records)
let keyCursor = await ageIndex.openKeyCursor();
while (keyCursor) {
console.log("Age:", keyCursor.key, "Primary key:", keyCursor.primaryKey);
keyCursor = await keyCursor.continue();
}Use modern async iteration syntax for convenient index traversal.
/**
* Iterate over the index using async iteration
* @returns Async iterable iterator over cursor values
*/
[Symbol.asyncIterator](): AsyncIterableIterator<
IDBPCursorWithValueIteratorValue<DBTypes, TxStores, StoreName, IndexName, Mode>
>;
/**
* Iterate over the records matching the query
* @param query - Index key or key range to match (optional)
* @param direction - Cursor direction (optional)
* @returns Async iterable iterator over cursor values
*/
iterate(
query?: IndexKey<DBTypes, StoreName, IndexName> | IDBKeyRange | null,
direction?: IDBCursorDirection
): AsyncIterableIterator<
IDBPCursorWithValueIteratorValue<DBTypes, TxStores, StoreName, IndexName, Mode>
>;Async Iteration Examples:
const tx = db.transaction("users", "readonly");
const userStore = tx.store;
const deptIndex = userStore.index("department");
const ageIndex = userStore.index("age");
// Iterate over all records in department
for await (const cursor of deptIndex.iterate("Engineering")) {
console.log("Engineer:", cursor.value.name, "Age:", cursor.value.age);
// Skip junior engineers
if (cursor.value.age < 25) {
cursor.continue();
}
}
// Iterate over age range with early termination
let found = false;
for await (const cursor of ageIndex.iterate(IDBKeyRange.bound(30, 40))) {
if (cursor.value.name === "Target Employee") {
console.log("Found target employee at age", cursor.key);
found = true;
break;
}
}
// Collect filtered results
const seniorEngineers = [];
for await (const cursor of deptIndex.iterate("Engineering")) {
if (cursor.value.age >= 40) {
seniorEngineers.push(cursor.value);
}
}
// Iterate in reverse order
const newestEmployees = [];
for await (const cursor of ageIndex.iterate(null, "prev")) {
newestEmployees.push(cursor.value);
if (newestEmployees.length >= 10) break; // Get 10 youngest
}
await tx.done;Common patterns for querying data using indexes.
Exact Match Queries:
const tx = db.transaction("users", "readonly");
const emailIndex = tx.store.index("email");
const nameIndex = tx.store.index("name");
// Single exact match
const user = await emailIndex.get("alice@company.com");
// Multiple exact matches
const alices = await nameIndex.getAll("Alice");Range Queries:
const ageIndex = tx.store.index("age");
// Age ranges
const youngAdults = await ageIndex.getAll(IDBKeyRange.bound(18, 25));
const seniors = await ageIndex.getAll(IDBKeyRange.lowerBound(65));
const minors = await ageIndex.getAll(IDBKeyRange.upperBound(17));
// Exclude boundaries
const middleAged = await ageIndex.getAll(IDBKeyRange.bound(30, 50, true, true));Pattern Matching:
const nameIndex = tx.store.index("name");
// Names starting with "A"
const aNames = await nameIndex.getAll(IDBKeyRange.bound("A", "B", false, true));
// Names starting with "Alice"
const aliceVariations = await nameIndex.getAll(IDBKeyRange.bound("Alice", "Alicf", false, true));Multi-Entry Index Queries:
const tagIndex = tx.store.index("tags"); // multiEntry: true
// Users with specific skill
const jsDevs = await tagIndex.getAll("javascript");
const reactDevs = await tagIndex.getAll("react");
// Count users with skill
const jsDevCount = await tagIndex.count("javascript");Compound Queries (using multiple indexes):
// Find young engineers
const deptIndex = tx.store.index("department");
const ageIndex = tx.store.index("age");
// Get all engineers
const allEngineers = await deptIndex.getAll("Engineering");
// Filter by age in memory (for complex conditions)
const youngEngineers = allEngineers.filter(user => user.age < 30);
// Alternative: Use cursor for memory efficiency
const youngEngineersAlt = [];
for await (const cursor of deptIndex.iterate("Engineering")) {
if (cursor.value.age < 30) {
youngEngineersAlt.push(cursor.value);
}
}Strategies for efficient index operations.
Choose Appropriate Index:
// Use most selective index first
const emailIndex = tx.store.index("email"); // Most selective (unique)
const deptIndex = tx.store.index("department"); // Less selective
const ageIndex = tx.store.index("age"); // Least selective
// Best: Start with most selective
const user = await emailIndex.get("specific@email.com");
// Good: Use department for departmental queries
const team = await deptIndex.getAll("Engineering");
// Acceptable: Use age for age-based queries
const adults = await ageIndex.getAll(IDBKeyRange.lowerBound(18));Key-Only Queries:
// More efficient when you only need to know if records exist
const hasEngineers = (await deptIndex.count("Engineering")) > 0;
// Get primary keys only
const engineerIds = await deptIndex.getAllKeys("Engineering");
// Instead of getting full records
const engineers = await deptIndex.getAll("Engineering");
const ids = engineers.map(e => e.id); // Less efficientLimit Result Sets:
// Use count parameter to limit results
const firstTenEngineers = await deptIndex.getAll("Engineering", 10);
// Use cursors for large datasets
let processed = 0;
for await (const cursor of deptIndex.iterate("Engineering")) {
processUser(cursor.value);
processed++;
if (processed >= 1000) break; // Process in batches
}