From 09690bd1798a0888ae595640292b3e6ee822e83a Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Fri, 17 Jan 2025 08:35:56 +0300 Subject: [PATCH] feat(react): add useNamedQuery --- packages/react/src/firestore/index.ts | 2 +- .../src/firestore/useNamedQuery.test.tsx | 141 ++++++++++++++++++ packages/react/src/firestore/useNamedQuery.ts | 27 ++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/firestore/useNamedQuery.test.tsx create mode 100644 packages/react/src/firestore/useNamedQuery.ts diff --git a/packages/react/src/firestore/index.ts b/packages/react/src/firestore/index.ts index ec75c7b1..f4294dc4 100644 --- a/packages/react/src/firestore/index.ts +++ b/packages/react/src/firestore/index.ts @@ -9,4 +9,4 @@ export { useDocumentQuery } from "./useDocumentQuery"; export { useCollectionQuery } from "./useCollectionQuery"; export { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery"; export { useGetCountFromServerQuery } from "./useGetCountFromServerQuery"; -// useNamedQuery +export { useNamedQuery } from "./useNamedQuery"; diff --git a/packages/react/src/firestore/useNamedQuery.test.tsx b/packages/react/src/firestore/useNamedQuery.test.tsx new file mode 100644 index 00000000..9a50f6f8 --- /dev/null +++ b/packages/react/src/firestore/useNamedQuery.test.tsx @@ -0,0 +1,141 @@ +import { describe, expect, test, beforeEach, vi } from "vitest"; +import { useNamedQuery } from "./useNamedQuery"; +import { renderHook, waitFor } from "@testing-library/react"; +import type * as FirestoreTypes from "firebase/firestore"; +import { firestore, wipeFirestore } from "~/testing-utils"; +import { wrapper, queryClient } from "../../utils"; + +// Mock the entire firebase/firestore module +vi.mock("firebase/firestore", async () => { + const actual = await vi.importActual( + "firebase/firestore" + ); + return { + collection: actual?.collection, + query: actual?.query, + where: actual?.where, + getFirestore: actual?.getFirestore, + connectFirestoreEmulator: actual?.connectFirestoreEmulator, + namedQuery: vi.fn(), + }; +}); + +// Import after mock definition +import { namedQuery, collection, query, where } from "firebase/firestore"; + +describe("useNamedQuery", () => { + beforeEach(async () => { + await wipeFirestore(); + queryClient.clear(); + vi.clearAllMocks(); + }); + + test("returns correct data for an existing named query", async () => { + const mockQuery = query( + collection(firestore, "test"), + where("field", "==", "value") + ); + vi.mocked(namedQuery).mockResolvedValue(mockQuery); + + const { result } = renderHook( + () => + useNamedQuery(firestore, "existingQuery", { + queryKey: ["named", "existing"], + }), + { wrapper } + ); + + expect(result.current.isLoading).toBe(true); + expect(result.current.data).toBeUndefined(); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toBe(mockQuery); + expect(namedQuery).toHaveBeenCalledWith(firestore, "existingQuery"); + expect(result.current.error).toBeNull(); + }); + + test("returns null for non-existent named query", async () => { + vi.mocked(namedQuery).mockResolvedValue(null); + + const { result } = renderHook( + () => + useNamedQuery(firestore, "nonExistentQuery", { + queryKey: ["named", "nonexistent"], + }), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.data).toBeNull(); + expect(namedQuery).toHaveBeenCalledWith(firestore, "nonExistentQuery"); + expect(result.current.error).toBeNull(); + }); + + test("handles error case properly", async () => { + const mockError = new Error("Query not found"); + vi.mocked(namedQuery).mockRejectedValue(mockError); + + const { result } = renderHook( + () => + useNamedQuery(firestore, "errorQuery", { + queryKey: ["named", "error"], + }), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBe(mockError); + expect(namedQuery).toHaveBeenCalledWith(firestore, "errorQuery"); + }); + + test("handles query options correctly", async () => { + const mockQuery = query(collection(firestore, "test")); + vi.mocked(namedQuery).mockResolvedValue(mockQuery); + + const { result } = renderHook( + () => + useNamedQuery(firestore, "optionsQuery", { + queryKey: ["named", "options"], + enabled: false, + }), + { wrapper } + ); + + expect(result.current.isLoading).toBe(false); + expect(result.current.isFetched).toBe(false); + expect(namedQuery).not.toHaveBeenCalled(); + }); + + test("handles refetching correctly", async () => { + const mockQuery = query(collection(firestore, "test")); + vi.mocked(namedQuery).mockResolvedValue(mockQuery); + + const { result } = renderHook( + () => + useNamedQuery(firestore, "refetchQuery", { + queryKey: ["named", "refetch"], + }), + { wrapper } + ); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + await result.current.refetch(); + + expect(namedQuery).toHaveBeenCalledTimes(2); + expect(result.current.data).toBe(mockQuery); + }); +}); diff --git a/packages/react/src/firestore/useNamedQuery.ts b/packages/react/src/firestore/useNamedQuery.ts new file mode 100644 index 00000000..cec6df8d --- /dev/null +++ b/packages/react/src/firestore/useNamedQuery.ts @@ -0,0 +1,27 @@ +import { useQuery, type UseQueryOptions } from "@tanstack/react-query"; +import { + type FirestoreError, + type Query, + type DocumentData, + namedQuery, + type Firestore, +} from "firebase/firestore"; + +type FirestoreUseQueryOptions = Omit< + UseQueryOptions, + "queryFn" +>; + +export function useNamedQuery< + AppModelType = DocumentData, + DbModelType extends DocumentData = DocumentData +>( + firestore: Firestore, + name: string, + options: FirestoreUseQueryOptions +) { + return useQuery({ + ...options, + queryFn: () => namedQuery(firestore, name), + }); +} \ No newline at end of file