Skip to content

Commit 49694b2

Browse files
HassanBahatiEhesp
andauthored
feat(react): add useNamedQuery (invertase#146)
Co-authored-by: Elliot Hesp <[email protected]>
1 parent e087b4f commit 49694b2

File tree

3 files changed

+169
-1
lines changed

3 files changed

+169
-1
lines changed

packages/react/src/firestore/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ export { useDocumentQuery } from "./useDocumentQuery";
88
export { useCollectionQuery } from "./useCollectionQuery";
99
export { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery";
1010
export { useGetCountFromServerQuery } from "./useGetCountFromServerQuery";
11-
// useNamedQuery
11+
export { useNamedQuery } from "./useNamedQuery";
1212
export { useDeleteDocumentMutation } from "./useDeleteDocumentMutation";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, expect, test, beforeEach, vi } from "vitest";
2+
import { useNamedQuery } from "./useNamedQuery";
3+
import { renderHook, waitFor } from "@testing-library/react";
4+
import type * as FirestoreTypes from "firebase/firestore";
5+
import { firestore, wipeFirestore } from "~/testing-utils";
6+
import { wrapper, queryClient } from "../../utils";
7+
8+
// Mock the entire firebase/firestore module
9+
vi.mock("firebase/firestore", async () => {
10+
const actual = await vi.importActual<typeof FirestoreTypes>(
11+
"firebase/firestore"
12+
);
13+
return {
14+
collection: actual?.collection,
15+
query: actual?.query,
16+
where: actual?.where,
17+
getFirestore: actual?.getFirestore,
18+
connectFirestoreEmulator: actual?.connectFirestoreEmulator,
19+
namedQuery: vi.fn(),
20+
};
21+
});
22+
23+
// Import after mock definition
24+
import { namedQuery, collection, query, where } from "firebase/firestore";
25+
26+
describe("useNamedQuery", () => {
27+
beforeEach(async () => {
28+
await wipeFirestore();
29+
queryClient.clear();
30+
vi.clearAllMocks();
31+
});
32+
33+
test("returns correct data for an existing named query", async () => {
34+
const mockQuery = query(
35+
collection(firestore, "test"),
36+
where("field", "==", "value")
37+
);
38+
vi.mocked(namedQuery).mockResolvedValue(mockQuery);
39+
40+
const { result } = renderHook(
41+
() =>
42+
useNamedQuery(firestore, "existingQuery", {
43+
queryKey: ["named", "existing"],
44+
}),
45+
{ wrapper }
46+
);
47+
48+
expect(result.current.isLoading).toBe(true);
49+
expect(result.current.data).toBeUndefined();
50+
51+
await waitFor(() => {
52+
expect(result.current.isLoading).toBe(false);
53+
expect(result.current.isSuccess).toBe(true);
54+
});
55+
56+
expect(result.current.data).toBe(mockQuery);
57+
expect(namedQuery).toHaveBeenCalledWith(firestore, "existingQuery");
58+
expect(result.current.error).toBeNull();
59+
});
60+
61+
test("returns null for non-existent named query", async () => {
62+
vi.mocked(namedQuery).mockResolvedValue(null);
63+
64+
const { result } = renderHook(
65+
() =>
66+
useNamedQuery(firestore, "nonExistentQuery", {
67+
queryKey: ["named", "nonexistent"],
68+
}),
69+
{ wrapper }
70+
);
71+
72+
await waitFor(() => {
73+
expect(result.current.isLoading).toBe(false);
74+
});
75+
76+
expect(result.current.data).toBeNull();
77+
expect(namedQuery).toHaveBeenCalledWith(firestore, "nonExistentQuery");
78+
expect(result.current.error).toBeNull();
79+
});
80+
81+
test("handles error case properly", async () => {
82+
const mockError = new Error("Query not found");
83+
vi.mocked(namedQuery).mockRejectedValue(mockError);
84+
85+
const { result } = renderHook(
86+
() =>
87+
useNamedQuery(firestore, "errorQuery", {
88+
queryKey: ["named", "error"],
89+
}),
90+
{ wrapper }
91+
);
92+
93+
await waitFor(() => {
94+
expect(result.current.isLoading).toBe(false);
95+
});
96+
97+
expect(result.current.data).toBeUndefined();
98+
expect(result.current.error).toBe(mockError);
99+
expect(namedQuery).toHaveBeenCalledWith(firestore, "errorQuery");
100+
});
101+
102+
test("handles query options correctly", async () => {
103+
const mockQuery = query(collection(firestore, "test"));
104+
vi.mocked(namedQuery).mockResolvedValue(mockQuery);
105+
106+
const { result } = renderHook(
107+
() =>
108+
useNamedQuery(firestore, "optionsQuery", {
109+
queryKey: ["named", "options"],
110+
enabled: false,
111+
}),
112+
{ wrapper }
113+
);
114+
115+
expect(result.current.isLoading).toBe(false);
116+
expect(result.current.isFetched).toBe(false);
117+
expect(namedQuery).not.toHaveBeenCalled();
118+
});
119+
120+
test("handles refetching correctly", async () => {
121+
const mockQuery = query(collection(firestore, "test"));
122+
vi.mocked(namedQuery).mockResolvedValue(mockQuery);
123+
124+
const { result } = renderHook(
125+
() =>
126+
useNamedQuery(firestore, "refetchQuery", {
127+
queryKey: ["named", "refetch"],
128+
}),
129+
{ wrapper }
130+
);
131+
132+
await waitFor(() => {
133+
expect(result.current.isLoading).toBe(false);
134+
});
135+
136+
await result.current.refetch();
137+
138+
expect(namedQuery).toHaveBeenCalledTimes(2);
139+
expect(result.current.data).toBe(mockQuery);
140+
});
141+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
2+
import {
3+
type FirestoreError,
4+
type Query,
5+
type DocumentData,
6+
namedQuery,
7+
type Firestore,
8+
} from "firebase/firestore";
9+
10+
type FirestoreUseQueryOptions<TData = unknown, TError = Error> = Omit<
11+
UseQueryOptions<TData, TError>,
12+
"queryFn"
13+
>;
14+
15+
export function useNamedQuery<
16+
AppModelType = DocumentData,
17+
DbModelType extends DocumentData = DocumentData
18+
>(
19+
firestore: Firestore,
20+
name: string,
21+
options: FirestoreUseQueryOptions<Query | null, FirestoreError>
22+
) {
23+
return useQuery<Query | null, FirestoreError>({
24+
...options,
25+
queryFn: () => namedQuery(firestore, name),
26+
});
27+
}

0 commit comments

Comments
 (0)