Skip to main content

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.
includeDeleted(): this
return
this
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.
onlyDeleted(): this
return
this
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
field
keyof T | string
required
The field to filter on
op
WhereOpsForValue<any>
required
The comparison operator. Available operators:
  • Equality: ==, !=
  • Comparison: <, <=, >, >=
  • Array: array-contains, array-contains-any
  • Membership: in, not-in
value
any
required
The value to compare against
return
this
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
K[]
required
Fields to include in the result
return
this
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
field
keyof T
required
The field to sort by
direction
'asc' | 'desc'
default:"'asc'"
Sort direction: ‘asc’ (default) or ‘desc’
return
this
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.
limit(n: number): this
n
number
required
Maximum number of documents to return
return
this
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>
data
Partial<T>
required
Partial document data (supports dot notation)
return
number
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>
return
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>
return
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>
return
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>
return
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>
field
keyof T
required
The numeric field to aggregate
operation
'sum' | 'avg'
required
‘sum’ or ‘avg’
return
number
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][]>
field
keyof T
required
The field to get distinct values from
return
T[K][]
Array of unique values
// 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');

Pagination

startAfterId

Start query results after a specific document ID. Used for cursor-based pagination.
async startAfterId(id: ID): Promise<this>
id
string
required
The document ID to start after
return
this
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;
}>
limit
number
required
Number of items per page
cursorId
string
ID of the last document from previous page
items
(T & { id: ID })[]
Array of documents for this page
nextCursorId
string | undefined
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;
}>
page
number
required
Page number (1-based)
pageSize
number
required
Number of items per page
items
(T & { id: ID })[]
Documents for this page
page
number
Current page number
pageSize
number
Items per page
total
number
Total number of matching documents
totalPages
number
Total number of pages
// 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;
}>
limit
number
required
Number of items per page
cursorId
string
ID of the last document from previous page
items
(T & { id: ID })[]
Documents for this page
nextCursorId
string | undefined
ID for the next page
total
number
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 })[]>
return
(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>
return
(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>
return
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 }>
yields
T & { id: ID }
Documents one at a time
// 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>
callback
(items) => void
required
Function called with updated results
onError
(error: Error) => void
Optional error handler
return
() => void
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();

Build docs developers (and LLMs) love