CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-dexie

A minimalistic wrapper for IndexedDB providing reactive queries, transactions, and schema management

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

query-building.mddocs/

Query Building

Advanced query building with indexed lookups, filtering, and result manipulation using WhereClause and Collection classes.

Capabilities

WhereClause - Index-Based Queries

The WhereClause provides methods for querying indexed fields with various comparison operators.

Equality Operations

/**
 * Matches records where the indexed field exactly equals the given value
 * @param value - Value to match exactly
 * @returns Collection of matching records
 */
equals(value: IndexableType): Collection<T, TKey, TInsertType>;

/**
 * Case-insensitive string equality match
 * @param value - String value to match (case-insensitive)
 * @returns Collection of matching records
 */
equalsIgnoreCase(value: string): Collection<T, TKey, TInsertType>;

/**
 * Matches records where the indexed field does not equal the given value
 * @param value - Value to exclude
 * @returns Collection of non-matching records
 */
notEqual(value: IndexableType): Collection<T, TKey, TInsertType>;

Usage Examples:

// Exact match
const alice = await db.friends.where("name").equals("Alice").toArray();

// Case-insensitive match
const users = await db.users.where("email").equalsIgnoreCase("ALICE@EXAMPLE.COM").toArray();

// Not equal
const nonAdults = await db.friends.where("age").notEqual(18).toArray();

Comparison Operations

/**
 * Matches records where the indexed field is greater than the given value
 * @param value - Minimum value (exclusive)
 * @returns Collection of matching records
 */
above(value: any): Collection<T, TKey, TInsertType>;

/**
 * Matches records where the indexed field is greater than or equal to the given value
 * @param value - Minimum value (inclusive)
 * @returns Collection of matching records
 */
aboveOrEqual(value: any): Collection<T, TKey, TInsertType>;

/**
 * Matches records where the indexed field is less than the given value
 * @param value - Maximum value (exclusive)
 * @returns Collection of matching records
 */
below(value: any): Collection<T, TKey, TInsertType>;

/**
 * Matches records where the indexed field is less than or equal to the given value
 * @param value - Maximum value (inclusive)
 * @returns Collection of matching records
 */
belowOrEqual(value: any): Collection<T, TKey, TInsertType>;

/**
 * Matches records where the indexed field is within the specified range
 * @param lower - Lower bound value
 * @param upper - Upper bound value
 * @param includeLower - Whether to include the lower bound (default: true)
 * @param includeUpper - Whether to include the upper bound (default: false)
 * @returns Collection of matching records
 */
between(
  lower: any, 
  upper: any, 
  includeLower?: boolean, 
  includeUpper?: boolean
): Collection<T, TKey, TInsertType>;

Usage Examples:

// Age comparisons
const adults = await db.friends.where("age").above(17).toArray();
const seniors = await db.friends.where("age").aboveOrEqual(65).toArray();
const minors = await db.friends.where("age").below(18).toArray();
const youngAdults = await db.friends.where("age").between(18, 30).toArray();

// Date ranges
const recentMessages = await db.messages
  .where("timestamp")
  .above(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours
  .toArray();

// String comparisons (alphabetical)
const namesAfterM = await db.friends.where("name").above("M").toArray();

// Range with custom bounds
const priceRange = await db.products
  .where("price")
  .between(10, 100, true, true) // Include both bounds
  .toArray();

String Operations

/**
 * Matches records where the indexed string field starts with the given prefix
 * @param prefix - String prefix to match
 * @returns Collection of matching records
 */
startsWith(prefix: string): Collection<T, TKey, TInsertType>;

/**
 * Case-insensitive prefix matching
 * @param prefix - String prefix to match (case-insensitive)
 * @returns Collection of matching records
 */
startsWithIgnoreCase(prefix: string): Collection<T, TKey, TInsertType>;

/**
 * Matches records where the indexed string field starts with any of the given prefixes
 * @param prefixes - Array of string prefixes to match
 * @returns Collection of matching records
 */
startsWithAnyOf(prefixes: string[]): Collection<T, TKey, TInsertType>;
startsWithAnyOf(...prefixes: string[]): Collection<T, TKey, TInsertType>;

/**
 * Case-insensitive version of startsWithAnyOf
 * @param prefixes - Array of string prefixes to match (case-insensitive)
 * @returns Collection of matching records
 */
startsWithAnyOfIgnoreCase(prefixes: string[]): Collection<T, TKey, TInsertType>;
startsWithAnyOfIgnoreCase(...prefixes: string[]): Collection<T, TKey, TInsertType>;

Usage Examples:

// Prefix matching
const aNames = await db.friends.where("name").startsWith("A").toArray();
const phoneNumbers = await db.contacts.where("phone").startsWith("+1").toArray();

// Case-insensitive prefix
const emails = await db.users.where("email").startsWithIgnoreCase("ADMIN").toArray();

// Multiple prefixes
const vowelNames = await db.friends
  .where("name")
  .startsWithAnyOf(["A", "E", "I", "O", "U"])
  .toArray();

// Case-insensitive multiple prefixes
const commonPrefixes = await db.products
  .where("name")
  .startsWithAnyOfIgnoreCase("pro", "pre", "super")
  .toArray();

Array Operations

/**
 * Matches records where the indexed field equals any of the given values
 * @param keys - Array of values to match
 * @returns Collection of matching records
 */
anyOf(keys: readonly IndexableType[]): Collection<T, TKey, TInsertType>;
anyOf(...keys: IndexableType[]): Collection<T, TKey, TInsertType>;

/**
 * Case-insensitive version of anyOf for string values
 * @param keys - Array of string values to match (case-insensitive)
 * @returns Collection of matching records
 */
anyOfIgnoreCase(keys: string[]): Collection<T, TKey, TInsertType>;
anyOfIgnoreCase(...keys: string[]): Collection<T, TKey, TInsertType>;

/**
 * Matches records where the indexed field does not equal any of the given values
 * @param keys - Array of values to exclude
 * @returns Collection of non-matching records
 */
noneOf(keys: readonly IndexableType[]): Collection<T, TKey, TInsertType>;

/**
 * Matches records where the indexed field falls within any of the given ranges
 * @param ranges - Array of range tuples [lower, upper]
 * @param options - Range inclusion options
 * @returns Collection of matching records
 */
inAnyRange(
  ranges: readonly { 0: any; 1: any }[], 
  options?: { includeLowers?: boolean; includeUppers?: boolean }
): Collection<T, TKey, TInsertType>;

Usage Examples:

// Match any of multiple values
const specificAges = await db.friends.where("age").anyOf([25, 30, 35]).toArray();
const specificIds = await db.friends.where("id").anyOf(1, 3, 5, 7).toArray();

// Case-insensitive any-of
const admins = await db.users
  .where("role")
  .anyOfIgnoreCase(["ADMIN", "admin", "Administrator"])
  .toArray();

// Exclude values
const nonTestUsers = await db.users
  .where("email")
  .noneOf(["test@example.com", "demo@example.com"])
  .toArray();

// Multiple ranges
const bizHours = await db.events
  .where("hour")
  .inAnyRange([[9, 12], [13, 17]], { includeLowers: true, includeUppers: false })
  .toArray();

// Age groups
const targetAges = await db.friends
  .where("age")
  .inAnyRange([[18, 25], [35, 45], [65, 75]])
  .toArray();

Collection - Result Manipulation

The Collection class provides methods for refining, manipulating, and executing queries.

Query Refinement

/**
 * Adds an additional filter condition using AND logic
 * @param predicate - Function that returns true for records to include
 * @returns Collection with additional filter applied
 */
and(predicate: (obj: T) => boolean): Collection<T, TKey, TInsertType>;

/**
 * Filters records using a predicate function
 * @param predicate - Function that returns true for records to include
 * @returns Collection of filtered records
 */
filter(predicate: (obj: T) => boolean): Collection<T, TKey, TInsertType>;

/**
 * Creates an OR condition with another index
 * @param indexName - Name of another index to query
 * @returns WhereClause for the alternate index
 */
or(indexName: string): WhereClause<T, TKey, TInsertType>;

/**
 * Limits the number of results returned
 * @param count - Maximum number of records to return
 * @returns Collection limited to specified count
 */
limit(count: number): Collection<T, TKey, TInsertType>;

/**
 * Skips the specified number of records
 * @param count - Number of records to skip
 * @returns Collection with records skipped
 */
offset(count: number): Collection<T, TKey, TInsertType>;

/**
 * Reverses the order of results
 * @returns Collection with reversed order
 */
reverse(): Collection<T, TKey, TInsertType>;

/**
 * Removes duplicate records
 * @returns Collection with duplicates removed
 */
distinct(): Collection<T, TKey, TInsertType>;

/**
 * Continues iteration until the predicate returns false
 * @param predicate - Function that returns false to stop iteration
 * @param includeStopEntry - Whether to include the record that failed the predicate
 * @returns Collection up to the stopping point
 */
until(predicate: (obj: T) => boolean, includeStopEntry?: boolean): Collection<T, TKey, TInsertType>;

Usage Examples:

// Combined filters
const activeYoungAdults = await db.friends
  .where("age")
  .between(18, 30)
  .and(friend => friend.status === "active")
  .toArray();

// Function-based filtering
const longNames = await db.friends
  .filter(friend => friend.name.length > 5)
  .toArray();

// Pagination
const page2 = await db.friends
  .orderBy("name")
  .offset(10)
  .limit(10)
  .toArray();

// Reverse order
const newestMessages = await db.messages
  .orderBy("timestamp")
  .reverse()
  .limit(10)
  .toArray();

// Remove duplicates
const uniqueAges = await db.friends
  .orderBy("age")
  .distinct()
  .toArray();

// Stop at condition
const friendsUntilAlice = await db.friends
  .orderBy("name")
  .until(friend => friend.name === "Alice", true)
  .toArray();

// OR conditions
const youngOrSenior = await db.friends
  .where("age")
  .below(25)
  .or("age")
  .above(65)
  .toArray();

Execution Methods

/**
 * Executes the query and returns all matching records as an array
 * @returns Promise resolving to array of matching records
 */
toArray(): Promise<T[]>;

/**
 * Counts the number of matching records without retrieving them
 * @returns Promise resolving to the count
 */
count(): Promise<number>;

/**
 * Returns the first matching record
 * @returns Promise resolving to the first record or undefined
 */
first(): Promise<T | undefined>;

/**
 * Returns the last matching record
 * @returns Promise resolving to the last record or undefined
 */
last(): Promise<T | undefined>;

/**
 * Iterates through matching records with a callback function
 * @param callback - Function called for each record
 * @returns Promise that resolves when iteration completes
 */
each(callback: (obj: T, cursor: { key: any; primaryKey: TKey }) => void | boolean): Promise<void>;

/**
 * Returns records sorted by the specified property (non-indexed sorting)
 * @param keyPath - Property path to sort by
 * @returns Promise resolving to sorted array
 */
sortBy(keyPath: string): Promise<T[]>;

Usage Examples:

// Get all results
const adults = await db.friends.where("age").above(17).toArray();

// Count without retrieving
const adultCount = await db.friends.where("age").above(17).count();

// Get first/last
const youngest = await db.friends.orderBy("age").first();
const oldest = await db.friends.orderBy("age").last();

// Iterate with callback
await db.friends.where("age").above(17).each((friend, cursor) => {
  console.log(`${friend.name} is ${friend.age} years old`);
  if (friend.name === "Alice") {
    return false; // Stop iteration
  }
});

// Non-indexed sorting
const sortedByName = await db.friends
  .where("age")
  .above(17)
  .sortBy("name");

Key Operations

/**
 * Returns the index keys of matching records
 * @returns Promise resolving to array of index keys
 */
keys(): Promise<IndexableType[]>;

/**
 * Returns the primary keys of matching records
 * @returns Promise resolving to array of primary keys
 */
primaryKeys(): Promise<TKey[]>;

/**
 * Returns unique index keys of matching records
 * @returns Promise resolving to array of unique index keys
 */
uniqueKeys(): Promise<IndexableType[]>;

Usage Examples:

// Get index keys
const ageKeys = await db.friends.where("age").above(17).keys();
console.log("Ages of adults:", ageKeys);

// Get primary keys for bulk operations
const adultIds = await db.friends.where("age").above(17).primaryKeys();
await db.friends.bulkUpdate(
  adultIds.map(id => ({ key: id, changes: { category: "adult" } }))
);

// Get unique keys
const uniqueAges = await db.friends.where("age").above(0).uniqueKeys();
console.log("All unique ages:", uniqueAges);

Mutation Methods

/**
 * Updates all matching records
 * @param changes - Changes to apply (object or function)
 * @returns Promise resolving to number of modified records
 */
modify(changes: UpdateSpec<T> | ((obj: T, ctx: { value: T }) => void)): Promise<number>;

/**
 * Deletes all matching records
 * @returns Promise resolving to number of deleted records
 */
delete(): Promise<number>;

Usage Examples:

// Update matching records
const updatedCount = await db.friends
  .where("age")
  .below(18)
  .modify({ category: "minor" });
console.log(`Updated ${updatedCount} records`);

// Update with function
await db.friends
  .where("status")
  .equals("inactive")
  .modify(friend => {
    friend.lastSeen = Date.now();
    friend.status = "away";
  });

// Update with PropModification
import { add } from "dexie";
await db.friends
  .where("age")
  .equals(25)
  .modify({ age: add(1) }); // Increment age by 1

// Delete matching records
const deletedCount = await db.friends
  .where("status")
  .equals("deleted")
  .delete();
console.log(`Deleted ${deletedCount} records`);

// Conditional deletion
await db.messages
  .where("timestamp")
  .below(Date.now() - 30 * 24 * 60 * 60 * 1000) // Older than 30 days
  .delete();

Complex Query Examples

// Multi-condition queries
const complexQuery = await db.products
  .where("category")
  .equals("electronics")
  .and(product => product.price > 100)
  .and(product => product.inStock)
  .filter(product => product.rating >= 4.0)
  .limit(10)
  .toArray();

// Pagination with search
const searchResults = await db.products
  .where("name")
  .startsWithIgnoreCase(searchTerm)
  .offset(pageNumber * pageSize)
  .limit(pageSize)
  .toArray();

// Range queries with sorting
const priceRangeQuery = await db.products
  .where("price")
  .between(minPrice, maxPrice)
  .reverse() // Highest price first
  .limit(20)
  .toArray();

// Multiple criteria with OR logic
const featuredProducts = await db.products
  .where("featured")
  .equals(1)
  .or("category")
  .equals("bestseller")
  .or("discount")
  .above(20)
  .toArray();

Install with Tessl CLI

npx tessl i tessl/npm-dexie

docs

database-management.md

error-handling.md

events.md

index.md

live-queries.md

query-building.md

schema-management.md

table-operations.md

utility-functions.md

tile.json