FirestoreQueryBuilder<T>
Fluent query builder for complex Firestore queries. Provides chainable methods for filtering, sorting, pagination, aggregation, and real-time updates.
Soft Delete Filters
includeDeleted
Include soft-deleted documents in query results. By default, soft-deleted documents (with deletedAt field) are excluded.
The query builder instance for chaining
// Get all users including soft-deleted ones
const allUsers = await userRepo.query()
.includeDeleted()
.get();
// Count all orders including deleted
const totalCount = await orderRepo.query()
.includeDeleted()
.count();
onlyDeleted
Query only soft-deleted documents. Useful for managing or recovering deleted data.
The query builder instance for chaining
// Find all deleted users
const deletedUsers = await userRepo.query()
.onlyDeleted()
.get();
// Count deleted orders from last month
const deletedCount = await orderRepo.query()
.onlyDeleted()
.where('deletedAt', '>', lastMonth)
.count();
Filtering
where
Add a where clause to filter documents. Supports various operators based on field type.
where<K extends keyof T | string, Op extends WhereOpsForValue<any>>(
field: K,
op: Op,
value: any
): this
op
WhereOpsForValue<any>
required
The comparison operator. Available operators:
- Equality:
==, !=
- Comparison:
<, <=, >, >=
- Array:
array-contains, array-contains-any
- Membership:
in, not-in
The value to compare against
The query builder instance for chaining
// Basic equality
await userRepo.query()
.where('status', '==', 'active')
.get();
// Comparison operators
await productRepo.query()
.where('price', '>', 100)
.where('stock', '>=', 10)
.get();
// Array operations
await postRepo.query()
.where('tags', 'array-contains', 'javascript')
.get();
// In/Not-in queries
await orderRepo.query()
.where('status', 'in', ['pending', 'processing'])
.get();
select
Select specific fields to reduce bandwidth and improve performance. Returns partial documents with only the specified fields.
select<K extends keyof T>(...fields: K[]): this
Fields to include in the result
The query builder instance for chaining
// Get only name and email for users
const users = await userRepo.query()
.select('name', 'email')
.get();
// Combine with where clause
const activeUserEmails = await userRepo.query()
.where('status', '==', 'active')
.select('email')
.get();
Sorting and Limiting
orderBy
Order query results by a specific field. Can be chained for multi-field sorting.
orderBy(field: keyof T, direction: 'asc' | 'desc' = 'asc'): this
direction
'asc' | 'desc'
default:"'asc'"
Sort direction: ‘asc’ (default) or ‘desc’
The query builder instance for chaining
// Sort users by creation date, newest first
const recentUsers = await userRepo.query()
.orderBy('createdAt', 'desc')
.limit(10)
.get();
// Multi-field sorting
const products = await productRepo.query()
.orderBy('category', 'asc')
.orderBy('price', 'desc')
.get();
limit
Limit the number of documents returned. Useful for pagination and performance optimization.
Maximum number of documents to return
The query builder instance for chaining
// Get top 5 products by price
const topProducts = await productRepo.query()
.orderBy('price', 'desc')
.limit(5)
.get();
// First page of results
const firstPage = await userRepo.query()
.orderBy('createdAt', 'desc')
.limit(20)
.get();
Bulk Operations
update
Update all documents matching the query. Supports dot notation for nested field updates.
async update(data: Partial<T>): Promise<number>
Partial document data (supports dot notation)
Number of documents updated
// Regular update
await ordersRepo.query()
.where('status', '==', 'pending')
.update({ status: 'shipped' });
// Dot notation for nested fields
await usersRepo.query()
.where('role', '==', 'admin')
.update({
'settings.notifications': true,
'profile.verified': true
});
// Mixed updates
await ordersRepo.query()
.where('category', '==', 'electronics')
.update({
discount: 0.1,
'metadata.updated': new Date().toISOString()
});
delete
Permanently delete all documents matching the query. This is a hard delete - documents cannot be recovered.
async delete(): Promise<number>
Number of documents deleted
// Delete all cancelled orders older than 30 days
const deletedCount = await orderRepo.query()
.where('status', '==', 'cancelled')
.where('createdAt', '<', thirtyDaysAgo)
.delete();
// Delete all test users
await userRepo.query()
.where('email', 'array-contains', '@test.com')
.delete();
softDelete
Soft delete all documents matching the query. Documents are marked with deletedAt timestamp but not removed.
async softDelete(): Promise<number>
Number of documents soft deleted
// Soft delete inactive users
const deletedCount = await userRepo.query()
.where('lastLogin', '<', oneYearAgo)
.softDelete();
// Soft delete products out of stock
await productRepo.query()
.where('stock', '==', 0)
.where('restockDate', '==', null)
.softDelete();
Counting and Aggregation
count
Count documents matching the query. More efficient than fetching all documents when you only need the count.
async count(): Promise<number>
Number of documents matching the query
// Count active users
const activeCount = await userRepo.query()
.where('status', '==', 'active')
.count();
// Count orders in date range
const orderCount = await orderRepo.query()
.where('createdAt', '>=', startDate)
.where('createdAt', '<=', endDate)
.count();
totalCount
Get total count of all documents in the collection. Ignores any where clauses but respects soft delete filter.
async totalCount(): Promise<number>
Total number of documents in the collection
// Get total user count (excluding soft deleted)
const total = await userRepo.query().totalCount();
// Get total including deleted
const totalWithDeleted = await userRepo.query()
.includeDeleted()
.totalCount();
aggregate
Perform aggregation operations on numeric fields. Currently supports sum and average calculations.
async aggregate(field: keyof T, operation: 'sum' | 'avg'): Promise<number>
The numeric field to aggregate
The calculated aggregate value
// Calculate total revenue
const totalRevenue = await orderRepo.query()
.where('status', '==', 'completed')
.aggregate('total', 'sum');
// Calculate average product rating
const avgRating = await reviewRepo.query()
.where('productId', '==', productId)
.aggregate('rating', 'avg');
distinctValues
Get all distinct values for a specific field. Useful for generating filter options or analyzing data distribution.
async distinctValues<K extends keyof T>(field: K): Promise<T[K][]>
The field to get distinct values from
// Get all product categories
const categories = await productRepo.query()
.distinctValues('category');
// Get all order statuses in use
const statuses = await orderRepo.query()
.where('createdAt', '>', lastMonth)
.distinctValues('status');
startAfterId
Start query results after a specific document ID. Used for cursor-based pagination.
async startAfterId(id: ID): Promise<this>
The document ID to start after
The query builder instance for chaining
// Get next page of results
const nextPage = await userRepo.query()
.orderBy('createdAt')
.startAfterId(lastUserId)
.limit(20)
.get();
paginate
Paginate through query results using cursor-based pagination. More efficient than offset pagination for large datasets.
async paginate(limit: number, cursorId?: ID): Promise<{
items: (T & { id: ID })[];
nextCursorId: ID | undefined;
}>
ID of the last document from previous page
Array of documents for this page
ID to use for the next page, or undefined if no more pages
// First page
const firstPage = await productRepo.query()
.where('category', '==', 'electronics')
.orderBy('price', 'desc')
.paginate(20);
// Next page
const nextPage = await productRepo.query()
.where('category', '==', 'electronics')
.orderBy('price', 'desc')
.paginate(20, firstPage.nextCursorId);
offsetPaginate
Paginate using offset/limit (traditional pagination). Less efficient than cursor pagination for large datasets.
async offsetPaginate(page: number, pageSize: number): Promise<{
items: (T & { id: ID })[];
page: number;
pageSize: number;
total: number;
totalPages: number;
}>
Total number of matching documents
// Get page 2 with 20 items per page
const results = await userRepo.query()
.where('role', '==', 'customer')
.orderBy('createdAt', 'desc')
.offsetPaginate(2, 20);
console.log(`Page ${results.page} of ${results.totalPages}`);
console.log(`Showing ${results.items.length} of ${results.total} total`);
paginateWithCount
Paginate with total count included. Combines paginate() and count() in a single method.
async paginateWithCount(
limit: number,
cursorId?: ID
): Promise<{
items: (T & { id: ID })[];
nextCursorId: ID | undefined;
total: number;
}>
ID of the last document from previous page
Total number of matching documents
// Get paginated results with progress info
const { items, nextCursorId, total } = await productRepo.query()
.where('inStock', '==', true)
.paginateWithCount(20, lastId);
console.log(`Showing ${items.length} of ${total} products`);
Fetching Results
get
Execute the query and return all matching documents. This is the main method to retrieve query results.
async get(): Promise<(T & { id: ID })[]>
Array of documents matching the query
// Simple query
const activeUsers = await userRepo.query()
.where('status', '==', 'active')
.get();
// Complex query with multiple conditions
const results = await orderRepo.query()
.where('status', '==', 'pending')
.where('total', '>', 100)
.where('createdAt', '>=', startOfDay)
.orderBy('createdAt', 'desc')
.limit(50)
.get();
getOne
Get a single document matching the query. Returns null if no documents match.
async getOne(): Promise<(T & { id: ID }) | null>
The first matching document or null
// Find user by email
const user = await userRepo.query()
.where('email', '==', '[email protected]')
.getOne();
// Get the cheapest product in category
const cheapest = await productRepo.query()
.where('category', '==', 'books')
.orderBy('price', 'asc')
.getOne();
exists
Check if any documents match the query. More efficient than count() when you only need to know if results exist.
async exists(): Promise<boolean>
True if at least one document matches
// Check if email is already taken
const emailExists = await userRepo.query()
.where('email', '==', newEmail)
.exists();
// Check if user has any orders
const hasOrders = await orderRepo.query()
.where('userId', '==', userId)
.exists();
Streaming and Real-time
stream
Stream query results as an async generator. Memory efficient for processing large datasets.
async *stream(): AsyncGenerator<T & { id: ID }>
// Process all users without loading into memory
for await (const user of userRepo.query().stream()) {
await sendEmail(user.email);
console.log(`Processed user ${user.id}`);
}
// Export data to CSV
const csvStream = createWriteStream('users.csv');
for await (const user of userRepo.query()
.where('subscribed', '==', true)
.stream()) {
csvStream.write(`${user.name},${user.email}\n`);
}
onSnapshot
Subscribe to real-time updates for documents matching the query. Callback is triggered whenever matching documents are added, modified, or removed.
async onSnapshot(
callback: (items: (T & { id: ID })[]) => void,
onError?: (error: Error) => void
): Promise<() => void>
Function called with updated results
Unsubscribe function to stop listening
// Monitor active orders in real-time
const unsubscribe = await orderRepo.query()
.where('status', '==', 'active')
.onSnapshot(
(orders) => {
console.log(`Active orders: ${orders.length}`);
updateDashboard(orders);
},
(error) => console.error('Snapshot error:', error)
);
// Later: stop listening
unsubscribe();