Skip to content

Querying

GET Methods

Retrieve a single document by a foreign key.

typescript
const user  = await repos.users.get.byDocId("user123");
const user2 = await repos.users.get.byEmail("alice@example.com");

// With raw DocumentSnapshot
const result = await repos.users.get.byDocId("user123", true);
if (result) {
  console.log(result.data); // UserModel
  console.log(result.doc);  // DocumentSnapshot
}

// Batch get
const users = await repos.users.get.byList("docId", ["u1", "u2", "u3"]);

QUERY Methods

Search for multiple documents by a query key.

typescript
const activeUsers = await repos.users.query.byIsActive(true);
const byName      = await repos.users.query.byName("Alice");

// With extra options
const results = await repos.users.query.byIsActive(true, {
  where:   [["age", ">=", 18]],
  orderBy: [{ field: "name", direction: "asc" }],
  limit:   50,
});

// Generic query
const users = await repos.users.query.by({
  where: [
    ["isActive", "==", true],
    ["age",      ">=", 18],
  ],
  orderBy: [{ field: "createdAt", direction: "desc" }],
  limit:  10,
  select: ["docId", "name", "email"],
});

// Get all
const all = await repos.users.query.getAll();

OR Conditions

orWhere — simple OR

Each clause is independently OR'd. Base where conditions are applied to every branch.

typescript
// status == "draft" OR status == "published"
const posts = await repos.posts.query.by({
  orWhere: [
    ["status", "==", "draft"],
    ["status", "==", "published"],
  ],
});

// (isActive == true) AND (userId == "A"  OR  userId == "B")
const posts2 = await repos.posts.query.by({
  where:   [["isActive", "==", true]],  // applied to every branch
  orWhere: [
    ["userId", "==", "user-A"],
    ["userId", "==", "user-B"],
  ],
});

orWhereGroups — compound OR (AND-within-OR)

typescript
// (status=="published" AND views>100) OR (status=="draft" AND userId=="me")
const posts = await repos.posts.query.by({
  orWhereGroups: [
    [["status", "==", "published"], ["views", ">", 100]],
    [["status", "==", "draft"],     ["userId", "==", myId]],
  ],
});

Under the hood

OR conditions are simulated by running one Firestore query per branch in parallel, then merging results in memory (dedup by document ID). No 30-disjunction limit applies.

in / array-contains-any operators with >30 values are automatically split into chunks of 30 queries.

QueryOptions reference

typescript
interface QueryOptions<T> {
  where?:         [keyof T, WhereFilterOp, any][];    // AND conditions
  orWhere?:       [keyof T, WhereFilterOp, any][];    // simple OR (one clause per entry)
  orWhereGroups?: [keyof T, WhereFilterOp, any][][];  // compound OR (AND groups)
  orderBy?:       { field: keyof T; direction?: "asc" | "desc" }[];
  limit?:         number;
  offset?:        number;
  select?:        (keyof T)[];                        // field projection
  startAt?:       DocumentSnapshot | any[];
  startAfter?:    DocumentSnapshot | any[];
  endAt?:         DocumentSnapshot | any[];
  endBefore?:     DocumentSnapshot | any[];
}

Pagination

Cursor-based pagination — efficient for large collections.

typescript
// First page with total count
const page1 = await repos.posts.query.paginate({
  pageSize:  10,
  orderBy:   [{ field: "createdAt", direction: "desc" }],
  withTotal: true,
});

// page1.data              → PostModel[]
// page1.hasNextPage       → boolean
// page1.hasPrevPage       → boolean
// page1.nextCursor        → DocumentSnapshot | undefined
// page1.prevCursor        → DocumentSnapshot | undefined
// page1.totalCount        → number (e.g. 142)
// page1.totalCountIsExact → boolean (true for AND queries, false for OR queries)

// Next page
const page2 = await repos.posts.query.paginate({
  pageSize:  10,
  cursor:    page1.nextCursor,
  direction: "next",  // default
});

// Previous page
const prev = await repos.posts.query.paginate({
  pageSize:  10,
  cursor:    page2.prevCursor,
  direction: "prev",
});

Server-side count performance (withTotal)

When withTotal: true is passed, total document count is computed via Firestore's native .count() server-side aggregation (1 index read per 1,000 documents). For orWhere and orWhereGroups, counts across branches are aggregated in parallel (fast estimation without fetching document payloads).

Paginate with filters and OR

typescript
const page = await repos.posts.query.paginate({
  pageSize: 10,
  where:   [["status", "==", "published"]],
  orWhere: [
    ["userId", "==", currentUserId],
    ["featured", "==", true],
  ],
  orderBy: [{ field: "createdAt", direction: "desc" }],
});

Paginate with include (populate relations per page)

typescript
const page = await repos.posts.query.paginate({
  pageSize: 10,
  include: [
    "docId",                                           // many → CommentModel[]
    { relation: "userId", select: ["docId", "name"] }, // one  → partial UserModel
  ],
});

for (const post of page.data) {
  console.log(post.populated.docId);  // CommentModel[]
  console.log(post.populated.userId); // { docId, name }
}

Iterate all pages — paginateAll

Async generator that automatically advances cursors. Ideal for migrations and exports.

typescript
for await (const page of repos.posts.query.paginateAll({ pageSize: 100 })) {
  console.log(`${page.data.length} posts on this page`);
}

// With include
for await (const page of repos.posts.query.paginateAll({
  pageSize: 100,
  include:  [{ relation: "userId", select: ["name"] }],
})) {
  for (const post of page.data) {
    console.log(post.populated.userId?.name);
  }
}

Real-time listener

typescript
const unsub = repos.users.query.onSnapshot(
  { where: [["isActive", "==", true]] },
  (users) => console.log(users),
  (err)   => console.error(err),
);

unsub(); // stop listening