# swr-firestore [![npm version](https://badge.fury.io/js/@tatsuokaniwa%2Fswr-firestore.svg)](https://badge.fury.io/js/@tatsuokaniwa%2Fswr-firestore) [![Node.js CI](https://github.com/t-k/swr-firestore/actions/workflows/test.yaml/badge.svg)](https://github.com/t-k/swr-firestore/actions/workflows/test.yaml) [![codecov](https://codecov.io/gh/t-k/swr-firestore/branch/main/graph/badge.svg?token=6WREEC5HKZ)](https://codecov.io/gh/t-k/swr-firestore) React Hooks library for Firestore, built using the Firebase v9 modular SDK. It utilizes the [`useSWRSubscription`](https://swr.vercel.app/docs/subscription) function from SWR library to enable subscription-based data fetching and caching. Inspired by [swr-firestore-v9](https://www.npmjs.com/package/swr-firestore-v9) ## Installation ```bash pnpm add @tatsuokaniwa/swr-firestore # or npm i --save @tatsuokaniwa/swr-firestore # or yarn add @tatsuokaniwa/swr-firestore ``` This package includes both the original root API and a tree-shaking-first `module` API. ### Requirements - Node.js >= 12.11 (requires `exports` field support in `package.json`) - TypeScript >= 4.7 (requires `exports` field support for type resolution) - `firebase >= 9.11.0`, `swr >= 2.1.0 < 3.0.0` - Use `firebase` for browser/client entry points and `firebase-admin` for server entry points. #### For Aggregation Queries - Client-side: `firebase >= 9.17.0` - Server-side: `firebase-admin >= 11.5.0` (recommended) ### Server fetcher security The `@tatsuokaniwa/swr-firestore/server` and `@tatsuokaniwa/swr-firestore/module/server` entry points use `firebase-admin`. They bypass Firestore Security Rules, just like any other Admin SDK code. Never import these server entry points into client bundles, and enforce authorization in your own server code before passing fetched data to SWR `fallback`. Do not build the `path` parameter directly from untrusted input. Firestore validates path syntax, but choosing which collection or document may be read is still your application's authorization responsibility. ## Usage ```tsx import { useCollection, useCollectionCount } from "@tatsuokaniwa/swr-firestore"; import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; initializeApp(); const db = getFirestore(); type Post = { content: string; status: "draft" | "published"; createdAt: Date; }; export default function App() { // Conditional Fetching const [isLogin, setIsLogin] = useState(false); const { data } = useCollection( isLogin && { path: "posts", where: [["status", "==", "published"]], orderBy: [["createdAt", "desc"]], parseDates: ["createdAt"], }, ); const { data: postCount } = useCollectionCount({ path: "posts", where: [["status", "==", "published"]], }); return (

{postCount} posts

{data?.map((x, i) => (
{x.content} {x.createdAt.toLocaleString()}
))}
); } ``` ### For more complex queries To perform complex queries like using `OR` queries or raw `QueryConstraint`, use the `queryConstraints` parameter. However, this method does not provide input completion for field names from type definitions. ```tsx import { or, orderBy, where } from "firebase/firestore"; useCollection({ path: "cities", queryConstraints: [ or(where("capital", "==", true), where("population", ">=", 1000000)), orderBy("createdAt", "desc"), ], }); ``` ### Tree-shaking-first `module` API `@tatsuokaniwa/swr-firestore/module` provides a `constraints`-based API. You import only the builders you need from subpath exports such as `where`, `orderBy`, `count`, and `average`, which makes it a better fit for tree-shaking with the Firebase modular SDK than the root API. ```tsx import { useCollection } from "@tatsuokaniwa/swr-firestore/module"; import { orderBy, where } from "@tatsuokaniwa/swr-firestore/module/query"; const constraints = [where("status", "==", "published"), orderBy("createdAt", "desc")]; const { data } = useCollection({ path: "posts", constraints, }); ``` See [README.module.md](./README.module.md) for the full module API guide, including SSR/SSG, aggregate APIs, and entrypoint details. ### Subscription error recovery Firestore stops a listener after an `onSnapshot` error. The subscription hooks forward that error through SWR, but they do not re-subscribe in place for the same key. For auth-gated reads, pass `null` until auth is ready so the hook mounts only after the user can read the path. ```tsx const { data, error } = useCollection( user ? { path: "posts", where: [["ownerId", "==", user.uid]], } : null, ); ``` ### SSG and SSR with the root API Use the root API when you want the original package entrypoints (`@tatsuokaniwa/swr-firestore` and `@tatsuokaniwa/swr-firestore/server`). The hooks should be rendered inside `SWRConfig` so the fallback data is actually consumed. ```tsx import { SWRConfig } from "swr"; import { useCollection, useGetDocs } from "@tatsuokaniwa/swr-firestore"; import { getCollection } from "@tatsuokaniwa/swr-firestore/server"; export async function getStaticProps() { const params = { path: "posts", where: [["status", "==", "published"]], }; const { key, data } = await getCollection({ ...params, isSubscription: true, // Add the prefix `$sub$` to the SWR key }); const { key: useGetDocsKey, data: useGetDocsData } = await getCollection(params); return { props: { fallback: { [key]: data, [useGetDocsKey]: useGetDocsData, }, }, }; } function Posts() { const { data } = useCollection({ path: "posts", where: [["status", "==", "published"]], }); const { data: useGetDocsData } = useGetDocs({ path: "posts", where: [["status", "==", "published"]], }); return ( <>

{useGetDocsData?.length ?? 0} documents

{data?.map((x, i) => (
{x.content}
))} ); } export default function Page({ fallback }: { fallback: Record }) { return ( ); } ``` For `OR` / `AND` queries in the root server module, use the server-only `filter` parameter. Unlike client-side `queryConstraints`, this API is JSON-serializable and works with SWR fallback keys. ```tsx import { getCollection } from "@tatsuokaniwa/swr-firestore/server"; const { key, data } = await getCollection({ path: "posts", filter: { type: "or", filters: [ { type: "where", field: "status", op: "==", value: "draft" }, { type: "where", field: "status", op: "==", value: "published" }, ], }, orderBy: [["createdAt", "desc"]], }); ``` ### SSG and SSR with the `module` API With `@tatsuokaniwa/swr-firestore/module` and its subpath exports, you can reuse the same client-side `constraints` when generating fallback keys on the server. As with the root API, render the hooks inside `SWRConfig` so the fallback data is consumed. ```tsx import { SWRConfig } from "swr"; import { useCollection } from "@tatsuokaniwa/swr-firestore/module"; import { where, orderBy } from "@tatsuokaniwa/swr-firestore/module/query"; import { getCollection } from "@tatsuokaniwa/swr-firestore/module/server"; const constraints = [where("status", "==", "published"), orderBy("createdAt", "desc")]; export async function getStaticProps() { const { key, data } = await getCollection({ path: "posts", constraints, isSubscription: true, }); return { props: { fallback: { [key]: data, }, }, }; } function Posts() { const { data } = useCollection({ path: "posts", constraints, }); return ( <> {data?.map((x, i) => (
{x.content}
))} ); } export default function Page({ fallback }: { fallback: Record }) { return ( ); } ``` ## API ### Entry points This library provides multiple entry points to minimize bundle size. Import only what you need: ```ts // All client-side hooks and fetchers (full bundle) import { useDoc, useGetDocs, fetchDoc } from "@tatsuokaniwa/swr-firestore"; // Real-time subscription hooks only (depends on swr/subscription) import { useCollection, useCollectionGroup, useDoc, } from "@tatsuokaniwa/swr-firestore/subscription"; // Aggregation hooks and fetchers only import { useAggregate, useCollectionCount, useCollectionGroupAggregate, useCollectionGroupCount, fetchAggregate, fetchCollectionCount, fetchCollectionGroupAggregate, fetchCollectionGroupCount, } from "@tatsuokaniwa/swr-firestore/aggregate"; // Server-side fetchers (Firebase Admin SDK) import { getCollection, getDoc } from "@tatsuokaniwa/swr-firestore/server"; // Tree-shaking-first module API import { useCollection as useModuleCollection } from "@tatsuokaniwa/swr-firestore/module"; import { where, orderBy } from "@tatsuokaniwa/swr-firestore/module/query"; ``` For the complete `module` API reference and examples, see [README.module.md](./README.module.md). The main public entry points are: - `@tatsuokaniwa/swr-firestore` full client-side API for the original root style - `@tatsuokaniwa/swr-firestore/subscription` root subscription hooks only - `@tatsuokaniwa/swr-firestore/aggregate` root aggregate/count hooks and fetchers - `@tatsuokaniwa/swr-firestore/server` root server-side fetchers - `@tatsuokaniwa/swr-firestore/module` tree-shaking-first client API - `@tatsuokaniwa/swr-firestore/module/query` typed builders for module constraints and aggregate fields - `@tatsuokaniwa/swr-firestore/module/subscription` module subscription hooks only - `@tatsuokaniwa/swr-firestore/module/aggregate` module aggregate/count hooks and client fetchers - `@tatsuokaniwa/swr-firestore/module/server` module server-side fetchers for SSR/SSG ### Full export list ```ts import { // SWR Hooks (real-time subscription, also available from ./subscription) useCollection, // Subscription for collection useCollectionGroup, // Subscription for collectionGroup useDoc, // Subscription for document // SWR Hooks (one-time fetch) useGetDocs, // Fetch documents with firestore's getDocs useGetDoc, // Fetch document with firestore's getDoc // SWR Hooks (aggregate, also available from ./aggregate) useCollectionCount, // Wrapper for getCountFromServer for collection useCollectionGroupCount, // Wrapper for getCountFromServer for collectionGroup useAggregate, // Aggregation queries (count, sum, average) for collection useCollectionGroupAggregate, // Aggregation queries for collectionGroup // Client-side fetchers (without SWR) fetchDoc, // Fetch single document fetchCollection, // Fetch collection fetchCollectionCount, // Count documents in collection (also from ./aggregate) fetchCollectionGroup, // Fetch collection group fetchCollectionGroupCount, // Count documents in collection group (also from ./aggregate) fetchAggregate, // Aggregation queries for collection (also from ./aggregate) fetchCollectionGroupAggregate, // Aggregation queries for collection group (also from ./aggregate) // Client-side transaction fetcher fetchDocInTx, // Fetch document within transaction } from "@tatsuokaniwa/swr-firestore"; import { getCollection, // Get the SWR key and data for useCollection, useGetDocs getCollectionCount, // for useCollectionCount getCollectionGroup, // for useCollectionGroup, useGetDocs getCollectionGroupCount, // for useCollectionGroupCount getAggregate, // for useAggregate getCollectionGroupAggregate, // for useCollectionGroupAggregate getDoc, // for useDoc, useGetDoc // Transaction-aware fetchers (for use within db.runTransaction) getDocInTx, getCollectionInTx, getCollectionCountInTx, getCollectionGroupInTx, getCollectionGroupCountInTx, } from "@tatsuokaniwa/swr-firestore/server"; ``` ### Type definitions for parameters ```ts import type { endAt, endBefore, limit, orderBy, startAfter, startAt, where, } from "firebase/firestore"; type DocumentId = "id"; // First argument of hook, specifies options to firestore, and is also used as a key for SWR. type KeyParams = | { // The path to the collection or document of Firestore. path: string; // `Paths` means object's property path, including nested object where?: [ Paths | DocumentId, // "id" is internally converted to documentId() Parameters[1], ValueOf | unknown, ][]; orderBy?: [Paths | DocumentId, Parameters[1]][]; startAt?: Parameters; startAfter?: Parameters; endAt?: Parameters; endBefore?: Parameters; limit?: number; limitToLast?: number; // Array of field names that should be parsed as dates. parseDates?: Paths[]; } // OR for more complex query | { // The path to the collection or document of Firestore. path: string; // raw query constraints from `firebase/firestore` queryConstraints?: | [QueryCompositeFilterConstraint, ...Array] | QueryConstraint[]; // Array of field names that should be parsed as dates. parseDates?: Paths[]; }; ``` ### Type definitions for return data ```ts import type { QueryDocumentSnapshot } from "firebase/firestore"; type DocumentData = T & { exists: boolean } & Pick; ``` ### `useCollection(params, swrOptions)` Subscription for collection #### Parameters - `params`: KeyParams | null - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values - `data`: data for given path's collection - `error`: FirestoreError ```ts import { useCollection } from "@tatsuokaniwa/swr-firestore"; const { data, error } = useCollection({ path: "posts", }); ``` ### `useCollectionCount(params, swrOptions)` Wrapper for getCountFromServer for collection #### Parameters - `params`: KeyParams except `parseDates` | null - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values Returns [`SWRResponse`](https://swr.vercel.app/docs/api#return-values) - `data`: number for given path's collection count result - `error`: FirestoreError - `isLoading`: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data" - `isValidating`: if there's a request or revalidation loading - `mutate(data?, options?)`: function to mutate the cached data (details) ```ts import { useCollectionCount } from "@tatsuokaniwa/swr-firestore"; const { data: postCount, error, isLoading, } = useCollectionCount({ path: "posts", }); ``` ### `useCollectionGroup(params, swrOptions)` Subscription for collectionGroup #### Parameters - `params`: KeyParams | null - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values - `data`: data for given path's collectionGroup - `error`: FirestoreError ### `useCollectionGroupCount(params, swrOptions)` Wrapper for getCountFromServer for collectionGroup #### Parameters - `params`: KeyParams except `parseDates` | null - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values Returns [`SWRResponse`](https://swr.vercel.app/docs/api#return-values) - `data`: number for given path's collectionGroup count result - `error`: FirestoreError - `isLoading`: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data" - `isValidating`: if there's a request or revalidation loading - `mutate(data?, options?)`: function to mutate the cached data (details) ### `useAggregate(params, swrOptions)` Wrapper for getAggregateFromServer for collection. Supports count, sum, and average aggregations in a single query. #### Parameters - `params`: KeyParams except `parseDates` & { aggregate: AggregateSpec } | null - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values Returns [`SWRResponse`](https://swr.vercel.app/docs/api#return-values) - `data`: aggregation result object with keys matching the aggregate spec - `error`: FirestoreError - `isLoading`: if there's an ongoing request and no "loaded data" - `isValidating`: if there's a request or revalidation loading - `mutate(data?, options?)`: function to mutate the cached data ```ts import { useAggregate } from "@tatsuokaniwa/swr-firestore"; type Product = { name: string; category: string; price: number; stock: number; }; const { data, error, isLoading } = useAggregate< Product, { totalStock: { type: "sum"; field: "stock" }; averagePrice: { type: "average"; field: "price" }; productCount: { type: "count" }; } >({ path: "products", where: [["category", "==", "electronics"]], aggregate: { totalStock: { type: "sum", field: "stock" }, averagePrice: { type: "average", field: "price" }, productCount: { type: "count" }, }, }); if (data) { console.log(data.productCount); // number console.log(data.totalStock); // number console.log(data.averagePrice); // number | null (null when no documents) } ``` ### `useCollectionGroupAggregate(params, swrOptions)` Wrapper for getAggregateFromServer for collectionGroup. Supports count, sum, and average aggregations across subcollections. #### Parameters - `params`: KeyParams except `parseDates` & { aggregate: AggregateSpec } | null - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values Returns [`SWRResponse`](https://swr.vercel.app/docs/api#return-values) - `data`: aggregation result object with keys matching the aggregate spec - `error`: FirestoreError - `isLoading`: if there's an ongoing request and no "loaded data" - `isValidating`: if there's a request or revalidation loading - `mutate(data?, options?)`: function to mutate the cached data ```ts import { useCollectionGroupAggregate } from "@tatsuokaniwa/swr-firestore"; type OrderItem = { productId: string; price: number; quantity: number; }; // Aggregate across all "items" subcollections const { data } = useCollectionGroupAggregate< OrderItem, { totalRevenue: { type: "sum"; field: "price" }; itemCount: { type: "count" }; } >({ path: "items", aggregate: { totalRevenue: { type: "sum", field: "price" }, itemCount: { type: "count" }, }, }); ``` ### `useDoc(params, swrOptions)` Subscription for document #### Parameters - `params`: KeyParams except `where`, `orderBy`, `limit` | null - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values - `data`: data for given path's document - `error`: FirestoreError ```ts import { useDoc } from "@tatsuokaniwa/swr-firestore"; const { data, error } = useDoc({ path: `posts/${postId}`, }); ``` ### `useGetDocs(params, swrOptions)` Fetch documents with firestore's [getDocs](https://firebase.google.com/docs/reference/js/firestore_.md#getdocs) function #### Parameters - `params`: KeyParams & { useOfflineCache?: boolean; isCollectionGroup?: boolean } | null set `isCollectionGroup: true` to get data from collectionGroup - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values Returns [`SWRResponse`](https://swr.vercel.app/docs/api#return-values) - `data`: data for given path's collection - `error`: FirestoreError - `isLoading`: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data" - `isValidating`: if there's a request or revalidation loading - `mutate(data?, options?)`: function to mutate the cached data (details) ```ts import { useGetDocs } from "@tatsuokaniwa/swr-firestore"; const { data, error } = useGetDocs({ path: `posts`, }); // for collectionGroup const { data, error } = useGetDocs({ path: `comments`, isCollectionGroup: true, }); ``` ### `useGetDoc(params, swrOptions)` Fetch the document with firestore's [getDoc](https://firebase.google.com/docs/reference/js/firestore_.md#getdoc) function #### Parameters - `params`: (KeyParams & { useOfflineCache?: boolean }) except `where`, `orderBy`, `limit` | null - `swrOptions`: [Options for SWR hook](https://swr.vercel.app/docs/api#options) except `fetcher` #### Return values Returns [`SWRResponse`](https://swr.vercel.app/docs/api#return-values) - `data`: data for given path's document - `error`: FirestoreError - `isLoading`: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data" - `isValidating`: if there's a request or revalidation loading - `mutate(data?, options?)`: function to mutate the cached data (details) ```ts import { useGetDoc } from "@tatsuokaniwa/swr-firestore"; const { data, error } = useGetDoc({ path: `posts/${postId}`, }); ``` ## Client-side fetchers These functions fetch data directly from Firestore without SWR caching. Useful for one-off data fetching, imperative data loading, or when you don't need SWR's caching and revalidation features. ### `fetchDoc(params)` Fetch a single document from Firestore ```ts import { fetchDoc } from "@tatsuokaniwa/swr-firestore"; const city = await fetchDoc({ path: "cities/tokyo", parseDates: ["createdAt"], }); ``` ### `fetchCollection(params)` Fetch documents from a collection ```ts import { fetchCollection } from "@tatsuokaniwa/swr-firestore"; const cities = await fetchCollection({ path: "cities", where: [["population", ">", 1000000]], orderBy: [["population", "desc"]], limit: 10, }); ``` ### `fetchCollectionCount(params)` Count documents in a collection ```ts import { fetchCollectionCount } from "@tatsuokaniwa/swr-firestore"; const count = await fetchCollectionCount({ path: "cities", where: [["population", ">", 1000000]], }); ``` ### `fetchCollectionGroup(params)` Fetch documents from a collection group ```ts import { fetchCollectionGroup } from "@tatsuokaniwa/swr-firestore"; const comments = await fetchCollectionGroup({ path: "comments", where: [["authorId", "==", "user123"]], limit: 10, }); ``` ### `fetchCollectionGroupCount(params)` Count documents in a collection group ```ts import { fetchCollectionGroupCount } from "@tatsuokaniwa/swr-firestore"; const count = await fetchCollectionGroupCount({ path: "comments", where: [["status", "==", "approved"]], }); ``` ### `fetchAggregate(params)` Fetch aggregation result from a collection ```ts import { fetchAggregate } from "@tatsuokaniwa/swr-firestore"; const result = await fetchAggregate< Product, { count: { type: "count" }; totalStock: { type: "sum"; field: "stock" }; avgPrice: { type: "average"; field: "price" }; } >({ path: "products", aggregate: { count: { type: "count" }, totalStock: { type: "sum", field: "stock" }, avgPrice: { type: "average", field: "price" }, }, }); ``` ### `fetchCollectionGroupAggregate(params)` Fetch aggregation result from a collection group ```ts import { fetchCollectionGroupAggregate } from "@tatsuokaniwa/swr-firestore"; const result = await fetchCollectionGroupAggregate< OrderItem, { totalRevenue: { type: "sum"; field: "price" } } >({ path: "items", aggregate: { totalRevenue: { type: "sum", field: "price" }, }, }); ``` ### `fetchDocInTx(transaction, params)` Fetch a single document within a Firestore transaction (client-side) Note: Due to Firebase client SDK limitations, only document fetching is supported in transactions. Collection queries within transactions are only available in the server module. ```ts import { getFirestore, runTransaction } from "firebase/firestore"; import { fetchDocInTx } from "@tatsuokaniwa/swr-firestore"; const db = getFirestore(); await runTransaction(db, async (t) => { const city = await fetchDocInTx(t, { path: "cities/tokyo", parseDates: ["createdAt"], }); if (city) { t.update(doc(db, "cities/tokyo"), { population: city.population + 1, }); } }); ``` ## Server module ### Server-only `filter` DSL The server module does not support client-side `queryConstraints` because it uses the Firebase Admin SDK. Existing server-side parameters such as `where`, `orderBy`, `startAt`, `startAfter`, `endAt`, `endBefore`, `limit`, and `limitToLast` still work as before. Use the JSON-serializable `filter` parameter only when you need `OR` / `AND` conditions on the server. ```ts type ServerFilter = | { type: "where"; field: Paths | "id"; // collectionGroup does not support "id" op: WhereFilterOp; value: unknown; } | { type: "or"; filters: [ServerFilter, ServerFilter, ...ServerFilter[]]; } | { type: "and"; filters: [ServerFilter, ServerFilter, ...ServerFilter[]]; }; ``` Example: ```ts import { getCollection } from "@tatsuokaniwa/swr-firestore/server"; const { key, data } = await getCollection({ path: "posts", filter: { type: "or", filters: [ { type: "where", field: "status", op: "==", value: "draft" }, { type: "where", field: "status", op: "==", value: "published" }, ], }, orderBy: [["createdAt", "desc"]], }); ``` ### `getCollection(params)` Fetch documents using the Firebase Admin SDK and return the SWR key and data #### Parameters - `params`: KeyParams #### Return values Returns `Promise<{ key: string; data: DocumentData[]; }>` - `key`: SWR Key - `data`: documents in the collection for the given path ```ts import { getCollection } from "@tatsuokaniwa/swr-firestore/server"; // For useCollection const { key, data } = await getCollection({ path: "posts", where: [["status", "==", "published"]], orderBy: [["createdAt", "desc"]], isSubscription: true, // Add the prefix `$sub$` to the SWR key }); // For useGetDocs const { key, data } = await getCollection({ path: "posts" }); // With OR filter const { data: filtered } = await getCollection({ path: "posts", filter: { type: "or", filters: [ { type: "where", field: "status", op: "==", value: "draft" }, { type: "where", field: "status", op: "==", value: "published" }, ], }, }); ``` ### `getCollectionCount(params)` Fetch document's count using the Firebase Admin SDK and return the SWR key and data #### Parameters - `params`: KeyParams except `parseDates` #### Return values Returns `Promise<{ key: string; data: number; }>` - `key`: SWR Key - `data`: number of documents in the collection for the given path. ```ts import { getCollectionCount } from "@tatsuokaniwa/swr-firestore/server"; // For useCollectionCount const { key, data } = await getCollectionCount({ path: "posts" }); ``` ### `getCollectionGroup(params)` Fetch documents using the Firebase Admin SDK and return the SWR key and data #### Parameters - `params`: KeyParams #### Return values Returns `Promise<{ key: string; data: DocumentData[]; }>` - `key`: SWR Key - `data`: documents in the collectionGroup for the given path ```ts import { getCollectionGroup } from "@tatsuokaniwa/swr-firestore/server"; // For useCollectionGroup const { key, data } = await getCollectionGroup({ path: "comments", where: [["content", "==", "foo"]], orderBy: [["createdAt", "desc"]], isSubscription: true, // Add the prefix `$sub$` to the SWR key }); // For useGetDocs with isCollectionGroup const { key, data } = await getCollectionGroup({ path: "comments" }); // With OR filter const { data: filtered } = await getCollectionGroup({ path: "comments", filter: { type: "or", filters: [ { type: "where", field: "content", op: "==", value: "foo" }, { type: "where", field: "content", op: "==", value: "bar" }, ], }, }); ``` ### `getCollectionGroupCount(params)` Fetch document's count using the Firebase Admin SDK and return the SWR key and data #### Parameters - `params`: KeyParams except `parseDates` #### Return values Returns `Promise<{ key: string; data: number; }>` - `key`: SWR Key - `data`: number of documents in the collection group for the given path ```ts import { getCollectionGroupCount } from "@tatsuokaniwa/swr-firestore/server"; // For useCollectionGroupCount const { key, data } = await getCollectionGroupCount({ path: "comments", }); ``` ### `getAggregate(params)` Fetch aggregation result using the Firebase Admin SDK and return the SWR key and data #### Parameters - `params`: KeyParams except `parseDates` & { aggregate: AggregateSpec } Note: `queryConstraints` is not supported on the server side because the Admin SDK uses a different query builder API. Typed parameters such as `where` and `orderBy` still work as before. Use the server-only `filter` parameter only when you need `OR` / `AND` conditions. #### Return values Returns `Promise<{ key: string; data: AggregateResult; }>` - `key`: SWR Key - `data`: aggregation result object ```ts import { getAggregate } from "@tatsuokaniwa/swr-firestore/server"; // For useAggregate const { key, data } = await getAggregate< Product, { count: { type: "count" }; totalRevenue: { type: "sum"; field: "price" }; } >({ path: "products", filter: { type: "or", filters: [ { type: "where", field: "category", op: "==", value: "electronics" }, { type: "where", field: "price", op: ">=", value: 400 }, ], }, aggregate: { count: { type: "count" }, totalRevenue: { type: "sum", field: "price" }, }, }); ``` ### `getCollectionGroupAggregate(params)` Fetch aggregation result across subcollections using the Firebase Admin SDK #### Parameters - `params`: KeyParams except `parseDates` & { aggregate: AggregateSpec } Note: `queryConstraints` is not supported on the server side because the Admin SDK uses a different query builder API. Typed parameters such as `where` and `orderBy` still work as before. Use the server-only `filter` parameter only when you need `OR` / `AND` conditions. #### Return values Returns `Promise<{ key: string; data: AggregateResult; }>` - `key`: SWR Key - `data`: aggregation result object ```ts import { getCollectionGroupAggregate } from "@tatsuokaniwa/swr-firestore/server"; // For useCollectionGroupAggregate const { key, data } = await getCollectionGroupAggregate< OrderItem, { totalItems: { type: "count" } } >({ path: "items", filter: { type: "or", filters: [ { type: "where", field: "name", op: "==", value: "Item A" }, { type: "where", field: "name", op: "==", value: "Item D" }, ], }, aggregate: { totalItems: { type: "count" }, }, }); ``` ### `getDoc(params)` Fetch the document using the Firebase Admin SDK and return the SWR key and data #### Parameters - `params`: KeyParams #### Return values Returns `Promise<{ key: string; data: DocumentData; }>` - `key`: SWR Key - `data`: data for given path's document ```ts import { getDoc } from "@tatsuokaniwa/swr-firestore/server"; // For useDoc const { key, data } = await getDoc({ path: `posts/${postId}`, isSubscription: true, // Add the prefix `$sub$` to the SWR key }); // For useGetDoc const { key, data } = await getDoc({ path: `posts/${postId}` }); ``` ### `getDocInTx(transaction, params)` Type-safe document fetcher for use within Firestore transactions #### Parameters - `transaction`: Firebase Admin SDK Transaction object - `params`: KeyParams except `where`, `orderBy`, `limit` #### Return values Returns `Promise | undefined>` - Returns the document data, or undefined if the document does not exist ```ts import { getFirestore } from "firebase-admin/firestore"; import { getDocInTx } from "@tatsuokaniwa/swr-firestore/server"; const db = getFirestore(); await db.runTransaction(async (t) => { const city = await getDocInTx(t, { path: "cities/tokyo", parseDates: ["createdAt"], }); if (city) { t.update(db.doc("cities/tokyo"), { population: city.population + 1, }); } }); ``` ### `getCollectionInTx(transaction, params)` Type-safe collection fetcher for use within Firestore transactions #### Parameters - `transaction`: Firebase Admin SDK Transaction object - `params`: KeyParams #### Return values Returns `Promise[]>` - Returns an array of document data ```ts import { getFirestore } from "firebase-admin/firestore"; import { getCollectionInTx } from "@tatsuokaniwa/swr-firestore/server"; const db = getFirestore(); await db.runTransaction(async (t) => { const cities = await getCollectionInTx(t, { path: "cities", where: [["population", ">", 1000000]], orderBy: [["population", "desc"]], limit: 10, }); cities.forEach((city) => { t.update(db.doc(`cities/${city.id}`), { isLargeCity: true, }); }); }); // OR / AND queries use filter await db.runTransaction(async (t) => { const cities = await getCollectionInTx(t, { path: "cities", filter: { type: "or", filters: [ { type: "where", field: "country", op: "==", value: "JP" }, { type: "where", field: "country", op: "==", value: "US" }, ], }, orderBy: [["population", "desc"]], limit: 10, }); cities.forEach((city) => { t.update(db.doc(`cities/${city.id}`), { isLargeCity: true, }); }); }); ``` ### `getCollectionCountInTx(transaction, params)` Type-safe collection count fetcher for use within Firestore transactions #### Parameters - `transaction`: Firebase Admin SDK Transaction object - `params`: KeyParams except `parseDates` #### Return values Returns `Promise` ```ts await db.runTransaction(async (t) => { const count = await getCollectionCountInTx(t, { path: "cities", where: [["population", ">", 1000000]], }); console.log(`Found ${count} large cities`); }); ``` ### `getCollectionGroupInTx(transaction, params)` Type-safe collection group fetcher for use within Firestore transactions #### Parameters - `transaction`: Firebase Admin SDK Transaction object - `params`: KeyParams #### Return values Returns `Promise[]>` ```ts await db.runTransaction(async (t) => { const comments = await getCollectionGroupInTx(t, { path: "comments", where: [["authorId", "==", "user123"]], limit: 10, }); // comments is DocumentData[] }); // OR / AND queries use filter await db.runTransaction(async (t) => { const comments = await getCollectionGroupInTx(t, { path: "comments", filter: { type: "or", filters: [ { type: "where", field: "authorId", op: "==", value: "user123" }, { type: "where", field: "authorId", op: "==", value: "user456" }, ], }, limit: 10, }); // comments is DocumentData[] }); ``` ### `getCollectionGroupCountInTx(transaction, params)` Type-safe collection group count fetcher for use within Firestore transactions #### Parameters - `transaction`: Firebase Admin SDK Transaction object - `params`: KeyParams except `parseDates` #### Return values Returns `Promise` ```ts await db.runTransaction(async (t) => { const count = await getCollectionGroupCountInTx(t, { path: "comments", where: [["status", "==", "approved"]], }); console.log(`Found ${count} approved comments`); }); ``` ### Custom Firestore Instance All functions and hooks accept an optional `db` parameter to use a specific Firestore instance instead of the default `getFirestore()`. ```typescript // Client-side const data = await fetchDoc({ path: "cities/tokyo", db: getFirestore(secondaryApp), }); // Server-side const { data } = await getDoc({ path: "cities/tokyo", db: getFirestore(adminApp), }); // React hooks const { data } = useDoc({ path: "cities/tokyo", db: getFirestore(secondaryApp), }); ``` When `db` is omitted, it falls back to `getFirestore()` as before, so existing code is not affected. ## Testing Before running the emulator-backed test suite locally, install a JDK 21 or newer and the [Firebase tools](https://firebase.google.com/docs/cli). CI uses Temurin 21. ```bash pnpm run test:ci ``` ## License MIT