Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useGetAggregateFromServerQuery hook #106

Merged
2 changes: 2 additions & 0 deletions packages/react/src/firestore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// useWriteBatchCommitMutation (WriteBatch)
export { useDocumentQuery } from "./useDocumentQuery";
export { useCollectionQuery } from "./useCollectionQuery";
// useGetCountFromServerQuery
export { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery";
export { useGetCountFromServerQuery } from "./useGetCountFromServerQuery";
// useGetAggregateFromServerQuery
// useNamedQuery
154 changes: 154 additions & 0 deletions packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { type ReactNode } from "react";
import { describe, expect, test, beforeEach } from "vitest";
import { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery";
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
collection,
addDoc,
query,
where,
sum,
average,
count,
} from "firebase/firestore";
import {
expectFirestoreError,
firestore,
wipeFirestore,
} from "~/testing-utils";

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});

const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe("useGetAggregateFromServerQuery", () => {
beforeEach(async () => await wipeFirestore());

test("returns correct count for empty collection", async () => {
const collectionRef = collection(firestore, "tests");

const { result } = renderHook(
() =>
useGetAggregateFromServerQuery(
collectionRef,
{ countOfDocs: count() },
{ queryKey: ["aggregate", "empty"] }
),
{ wrapper }
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

Check failure on line 49 in packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx

View workflow job for this annotation

GitHub Actions / test

packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx > useGetAggregateFromServerQuery > returns correct count for empty collection

AssertionError: expected false to be true // Object.is equality Ignored nodes: comments, script, style <html> <head /> <body> <p class="firebase-emulator-warning" style="position: fixed; width: 100%; background-color: #ffffff; border: 0.1em solid #000000; color: #b50000; bottom: 0px; left: 0px; margin: 0px; z-index: 10000; text-align: center;" > Running in emulator mode. Do not use with production credentials. </p> <div /> </body> </html> - Expected + Received - true + false ❯ packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx:49:58 ❯ runWithExpensiveErrorDiagnosticsDisabled node_modules/.pnpm/@testing-library[email protected]/node_modules/@testing-library/dom/dist/config.js:47:12 ❯ checkCallback node_modules/.pnpm/@testing-library[email protected]/node_modules/@testing-library/dom/dist/wait-for.js:124:77 ❯ Timeout.checkRealTimersCallback node_modules/.pnpm/@testing-library[email protected]/node_modules/@testing-library/dom/dist/wait-for.js:118:16

expect(result.current.data?.data().countOfDocs).toBe(0);
});

test("returns correct aggregate values for non-empty collection", async () => {
const collectionRef = collection(firestore, "tests");

await addDoc(collectionRef, { value: 10 });
await addDoc(collectionRef, { value: 20 });
await addDoc(collectionRef, { value: 30 });

const { result } = renderHook(
() =>
useGetAggregateFromServerQuery(
collectionRef,
{
countOfDocs: count(),
averageValue: average("value"),
totalValue: sum("value"),
},
{ queryKey: ["aggregate", "non-empty"] }
),
{ wrapper }
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

expect(result.current.data?.data().averageValue).toBe(20);
expect(result.current.data?.data().totalValue).toBe(60);
expect(result.current.data?.data().countOfDocs).toBe(3);
});

test("handles complex queries", async () => {
const collectionRef = collection(firestore, "tests");

await addDoc(collectionRef, { category: "A", books: 10 });
await addDoc(collectionRef, { category: "B", books: 20 });
await addDoc(collectionRef, { category: "A", books: 30 });
await addDoc(collectionRef, { category: "C", books: 40 });

const complexQuery = query(collectionRef, where("category", "==", "A"));

const { result } = renderHook(
() =>
useGetAggregateFromServerQuery(
complexQuery,
{
countOfDocs: count(),
averageNumberOfBooks: average("books"),
totalNumberOfBooks: sum("books"),
},
{ queryKey: ["aggregate", "complex"] }
),
{
wrapper,
}
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

expect(result.current.data?.data().averageNumberOfBooks).toBe(20);

Check failure on line 110 in packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx

View workflow job for this annotation

GitHub Actions / test

packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx > useGetAggregateFromServerQuery > handles complex queries

AssertionError: expected null to be 20 // Object.is equality - Expected: 20 + Received: null ❯ packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx:110:62
expect(result.current.data?.data().totalNumberOfBooks).toBe(40);
expect(result.current.data?.data().countOfDocs).toBe(2);
});

test("handles restricted collection appropriately", async () => {
const collectionRef = collection(firestore, "restrictedCollection");

const { result } = renderHook(
() =>
useGetAggregateFromServerQuery(
collectionRef,
{ count: count() },
{ queryKey: ["aggregate", "restricted"] }
),
{ wrapper }
);

await waitFor(() => expect(result.current.isError).toBe(true));

expectFirestoreError(result.current.error, "permission-denied");
});

test("returns pending state initially", async () => {
const collectionRef = collection(firestore, "tests");

await addDoc(collectionRef, { value: 10 });

const { result } = renderHook(
() =>
useGetAggregateFromServerQuery(
collectionRef,
{ count: count() },
{ queryKey: ["aggregate", "pending"] }
),
{ wrapper }
);

expect(result.current.isPending).toBe(true);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

expect(result.current.data?.data().count).toBe(1);

Check failure on line 152 in packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx

View workflow job for this annotation

GitHub Actions / test

packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx > useGetAggregateFromServerQuery > returns pending state initially

AssertionError: expected +0 to be 1 // Object.is equality - Expected + Received - 1 + 0 ❯ packages/react/src/firestore/useGetAggregateFromServerQuery.test.tsx:152:47
});
});
35 changes: 35 additions & 0 deletions packages/react/src/firestore/useGetAggregateFromServerQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import {
type Query,
type FirestoreError,
getAggregateFromServer,
type AggregateSpec,
type DocumentData,
type AggregateQuerySnapshot,
} from "firebase/firestore";

type FirestoreUseQueryOptions<TData = unknown, TError = Error> = Omit<
UseQueryOptions<TData, TError>,
"queryFn"
>;

export function useGetAggregateFromServerQuery<
T extends AggregateSpec,
AppModelType = DocumentData,
DbModelType extends DocumentData = DocumentData
>(
query: Query<AppModelType, DbModelType>,
aggregateSpec: T,
options: FirestoreUseQueryOptions<
AggregateQuerySnapshot<T, AppModelType, DbModelType>,
FirestoreError
>
) {
return useQuery<
AggregateQuerySnapshot<T, AppModelType, DbModelType>,
FirestoreError
>({
...options,
queryFn: () => getAggregateFromServer(query, aggregateSpec),
});
}
Loading