Advanced querying capabilities with key ranges, bounds, and filtering for precise data selection.
Factory methods for creating key ranges to filter queries and cursor iterations.
interface IDBKeyRangeStatic {
/**
* Creates a range containing only the specified key
* @param value - The single key value to match
* @returns Key range matching exactly the given value
*/
only(value: IDBValidKey): IDBKeyRange;
/**
* Creates a range with a lower bound
* @param lower - The lower bound key
* @param open - Whether lower bound is exclusive (default: false)
* @returns Key range from lower bound to end
*/
lowerBound(lower: IDBValidKey, open?: boolean): IDBKeyRange;
/**
* Creates a range with an upper bound
* @param upper - The upper bound key
* @param open - Whether upper bound is exclusive (default: false)
* @returns Key range from start to upper bound
*/
upperBound(upper: IDBValidKey, open?: boolean): IDBKeyRange;
/**
* Creates a range with both lower and upper bounds
* @param lower - The lower bound key
* @param upper - The upper bound key
* @param lowerOpen - Whether lower bound is exclusive (default: false)
* @param upperOpen - Whether upper bound is exclusive (default: false)
* @returns Key range between the bounds
*/
bound(
lower: IDBValidKey,
upper: IDBValidKey,
lowerOpen?: boolean,
upperOpen?: boolean
): IDBKeyRange;
}Represents a range of keys for querying and filtering operations.
interface IDBKeyRange {
/** Lower bound value (readonly) */
readonly lower: IDBValidKey | undefined;
/** Upper bound value (readonly) */
readonly upper: IDBValidKey | undefined;
/** Whether lower bound is exclusive (readonly) */
readonly lowerOpen: boolean;
/** Whether upper bound is exclusive (readonly) */
readonly upperOpen: boolean;
/**
* Tests whether a key is included in this range
* @param key - Key to test
* @returns True if key is within the range
*/
includes(key: IDBValidKey): boolean;
}Usage Examples:
import "fake-indexeddb/auto";
// Single value range
const exactRange = IDBKeyRange.only("electronics");
store.index("by_category").getAll(exactRange);
// Lower bound range (>= 100)
const minPriceRange = IDBKeyRange.lowerBound(100);
store.index("by_price").getAll(minPriceRange);
// Lower bound range (> 100, exclusive)
const aboveMinRange = IDBKeyRange.lowerBound(100, true);
store.index("by_price").getAll(aboveMinRange);
// Upper bound range (<= 500)
const maxPriceRange = IDBKeyRange.upperBound(500);
store.index("by_price").getAll(maxPriceRange);
// Upper bound range (< 500, exclusive)
const belowMaxRange = IDBKeyRange.upperBound(500, true);
store.index("by_price").getAll(belowMaxRange);
// Bounded range (100 <= price <= 500)
const priceRange = IDBKeyRange.bound(100, 500);
store.index("by_price").getAll(priceRange);
// Bounded range (100 < price < 500, both exclusive)
const exclusiveRange = IDBKeyRange.bound(100, 500, true, true);
store.index("by_price").getAll(exclusiveRange);// Query records within date range
const startDate = new Date("2024-01-01");
const endDate = new Date("2024-12-31");
const dateRange = IDBKeyRange.bound(startDate, endDate);
const tx = db.transaction("orders", "readonly");
const store = tx.objectStore("orders");
const dateIndex = store.index("by_date");
dateIndex.getAll(dateRange).onsuccess = (event) => {
const orders = event.target.result;
console.log(`Orders from 2024: ${orders.length}`);
};
// Last 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const recentRange = IDBKeyRange.lowerBound(thirtyDaysAgo);
dateIndex.getAll(recentRange).onsuccess = (event) => {
const recentOrders = event.target.result;
console.log(`Recent orders: ${recentOrders.length}`);
};// Query with compound keys [category, price]
const categoryPriceIndex = store.index("category_price");
// Electronics under $1000
const electronicsRange = IDBKeyRange.bound(
["electronics", 0],
["electronics", 1000]
);
categoryPriceIndex.getAll(electronicsRange).onsuccess = (event) => {
const affordableElectronics = event.target.result;
console.log("Affordable electronics:", affordableElectronics);
};
// All books (any price)
const booksRange = IDBKeyRange.bound(
["books", Number.NEGATIVE_INFINITY],
["books", Number.POSITIVE_INFINITY]
);
// Or more simply for all books:
const allBooksRange = IDBKeyRange.bound(
["books"],
["books", []] // Empty array is greater than any other value
);// String prefix matching
const nameIndex = store.index("by_name");
// Products starting with "Lap" (like "Laptop")
const prefixRange = IDBKeyRange.bound("Lap", "Lap\uffff");
nameIndex.getAll(prefixRange).onsuccess = (event) => {
const laptops = event.target.result;
console.log("Products starting with 'Lap':", laptops);
};
// Case-insensitive range (if data is normalized to lowercase)
const searchTerm = "gaming";
const caseInsensitiveRange = IDBKeyRange.bound(
searchTerm,
searchTerm + "\uffff"
);
nameIndex.getAll(caseInsensitiveRange).onsuccess = (event) => {
const gamingProducts = event.target.result;
console.log("Gaming products:", gamingProducts);
};// Price ranges for different budgets
const priceIndex = store.index("by_price");
const budgetRanges = {
budget: IDBKeyRange.upperBound(100), // <= $100
midRange: IDBKeyRange.bound(100, 500, true), // $100 < price <= $500
premium: IDBKeyRange.bound(500, 2000, true), // $500 < price <= $2000
luxury: IDBKeyRange.lowerBound(2000, true) // > $2000
};
Object.entries(budgetRanges).forEach(([category, range]) => {
priceIndex.count(range).onsuccess = (event) => {
console.log(`${category} products: ${event.target.result}`);
};
});// Test if key is in range
const priceRange = IDBKeyRange.bound(100, 500);
console.log(priceRange.includes(150)); // true
console.log(priceRange.includes(50)); // false
console.log(priceRange.includes(600)); // false
console.log(priceRange.includes(100)); // true (inclusive by default)
console.log(priceRange.includes(500)); // true (inclusive by default)
// Test with exclusive bounds
const exclusiveRange = IDBKeyRange.bound(100, 500, true, true);
console.log(exclusiveRange.includes(100)); // false (exclusive)
console.log(exclusiveRange.includes(500)); // false (exclusive)
console.log(exclusiveRange.includes(300)); // true// Complex filtering using cursor + key ranges
function findProductsWithConditions(store, conditions) {
return new Promise((resolve) => {
const results = [];
const { category, minPrice, maxPrice, inStock } = conditions;
// Use compound index for category + price
const categoryPriceIndex = store.index("category_price");
const range = IDBKeyRange.bound(
[category, minPrice],
[category, maxPrice]
);
categoryPriceIndex.openCursor(range).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const product = cursor.value;
// Additional filtering in memory
if (!inStock || product.stock > 0) {
results.push(product);
}
cursor.continue();
} else {
resolve(results);
}
};
});
}
// Usage
const tx = db.transaction("products", "readonly");
const store = tx.objectStore("products");
findProductsWithConditions(store, {
category: "electronics",
minPrice: 200,
maxPrice: 1000,
inStock: true
}).then((products) => {
console.log("Filtered products:", products);
});// Paginate within a key range
function getPaginatedRange(index, keyRange, pageSize, lastKey = null) {
return new Promise((resolve) => {
const results = [];
let count = 0;
// Adjust range to start after last key
let effectiveRange = keyRange;
if (lastKey) {
if (keyRange.upper !== undefined) {
effectiveRange = IDBKeyRange.bound(
lastKey,
keyRange.upper,
true, // Exclude lastKey
keyRange.upperOpen
);
} else {
effectiveRange = IDBKeyRange.lowerBound(lastKey, true);
}
}
index.openCursor(effectiveRange).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && count < pageSize) {
results.push({
key: cursor.key,
value: cursor.value
});
count++;
cursor.continue();
} else {
resolve({
results,
hasMore: cursor !== null,
lastKey: results.length > 0 ? results[results.length - 1].key : null
});
}
};
});
}
// Usage - paginate expensive products
const priceIndex = store.index("by_price");
const expensiveRange = IDBKeyRange.lowerBound(1000);
async function paginateExpensiveProducts() {
let lastKey = null;
let page = 1;
do {
const result = await getPaginatedRange(priceIndex, expensiveRange, 10, lastKey);
console.log(`Page ${page}:`, result.results.map(r =>
`${r.value.name} - $${r.key}`
));
lastKey = result.lastKey;
page++;
} while (result.hasMore);
}// Simulate OR queries by combining multiple ranges
async function getMultipleRanges(index, ranges) {
const allResults = [];
for (const range of ranges) {
const results = await new Promise((resolve) => {
const rangeResults = [];
index.openCursor(range).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
rangeResults.push(cursor.value);
cursor.continue();
} else {
resolve(rangeResults);
}
};
});
allResults.push(...results);
}
// Remove duplicates based on primary key
const uniqueResults = allResults.filter((product, index, array) =>
array.findIndex(p => p.id === product.id) === index
);
return uniqueResults;
}
// Usage - get both budget and luxury products
const priceIndex = store.index("by_price");
const ranges = [
IDBKeyRange.upperBound(100), // Budget: <= $100
IDBKeyRange.lowerBound(2000) // Luxury: >= $2000
];
getMultipleRanges(priceIndex, ranges).then((products) => {
console.log("Budget and luxury products:", products);
});// Efficient key range usage
const priceIndex = store.index("by_price");
// Good - use specific range
const specificRange = IDBKeyRange.bound(100, 500);
priceIndex.getAll(specificRange);
// Better - use count() first for large datasets
priceIndex.count(specificRange).onsuccess = (event) => {
const count = event.target.result;
if (count < 1000) {
// Small result set - get all
priceIndex.getAll(specificRange);
} else {
// Large result set - use cursor with pagination
priceIndex.openCursor(specificRange);
}
};
// Best - combine with limits
priceIndex.getAll(specificRange, 50).onsuccess = (event) => {
const firstFifty = event.target.result;
// Process first 50 results
};// Handle invalid key ranges
try {
// This will throw DataError - lower > upper
const invalidRange = IDBKeyRange.bound(500, 100);
} catch (error) {
if (error.name === "DataError") {
console.error("Invalid key range - lower bound greater than upper bound");
}
}
// Validate keys before creating ranges
function createSafeRange(lower, upper) {
try {
// Normalize and validate keys
if (lower != null && upper != null) {
// Simple comparison for same types
if (typeof lower === typeof upper && lower > upper) {
throw new Error("Lower bound cannot be greater than upper bound");
}
}
return IDBKeyRange.bound(lower, upper);
} catch (error) {
console.error("Failed to create key range:", error.message);
return null;
}
}
// Usage
const range = createSafeRange(userInputMin, userInputMax);
if (range) {
priceIndex.getAll(range);
}