From b5bf53057f35b51e97e047da8abdd60c990f2bba Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Fri, 31 Jan 2025 05:28:56 +0300 Subject: [PATCH 1/2] react(firestore): add useAddDocumentMutation --- packages/react/src/firestore/index.ts | 1 + .../firestore/useAddDocumentMutation.test.tsx | 145 ++++++++++++++++++ .../src/firestore/useAddDocumentMutation.ts | 33 ++++ 3 files changed, 179 insertions(+) create mode 100644 packages/react/src/firestore/useAddDocumentMutation.test.tsx create mode 100644 packages/react/src/firestore/useAddDocumentMutation.ts diff --git a/packages/react/src/firestore/index.ts b/packages/react/src/firestore/index.ts index ec75c7b1..55b5842d 100644 --- a/packages/react/src/firestore/index.ts +++ b/packages/react/src/firestore/index.ts @@ -10,3 +10,4 @@ export { useCollectionQuery } from "./useCollectionQuery"; export { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery"; export { useGetCountFromServerQuery } from "./useGetCountFromServerQuery"; // useNamedQuery +export { useAddDocumentMutation } from "./useAddDocumentMutation"; diff --git a/packages/react/src/firestore/useAddDocumentMutation.test.tsx b/packages/react/src/firestore/useAddDocumentMutation.test.tsx new file mode 100644 index 00000000..319cf835 --- /dev/null +++ b/packages/react/src/firestore/useAddDocumentMutation.test.tsx @@ -0,0 +1,145 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { + collection, + type CollectionReference, + type DocumentReference, + getDoc, +} from "firebase/firestore"; +import { beforeEach, describe, expect, test } from "vitest"; +import { useAddDocumentMutation } from "./useAddDocumentMutation"; + +import { + expectFirestoreError, + firestore, + wipeFirestore, +} from "~/testing-utils"; +import { queryClient, wrapper } from "../../utils"; +import { act } from "react"; + +describe("useAddDocumentMutation", () => { + beforeEach(async () => { + await wipeFirestore(); + queryClient.clear(); + }); + + test("successfully adds a document", async () => { + const collectionRef = collection(firestore, "tests"); + const testData = { foo: "bar", num: 42 }; + + const { result } = renderHook( + () => useAddDocumentMutation(collectionRef, testData), + { wrapper } + ); + + expect(result.current.isIdle).toBe(true); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toBeDefined(); + const docRef = result.current.data!; + + const snapshot = await getDoc(docRef); + expect(snapshot.exists()).toBe(true); + expect(snapshot.data()).toEqual(testData); + }); + + test("handles type-safe data", async () => { + interface TestDoc { + foo: string; + num: number; + } + + const collectionRef = collection( + firestore, + "tests" + ) as CollectionReference; + const testData: TestDoc = { foo: "test", num: 123 }; + + const { result } = renderHook( + () => useAddDocumentMutation(collectionRef, testData), + { wrapper } + ); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = await getDoc(result.current.data!); + const data = snapshot.data(); + expect(data?.foo).toBe("test"); + expect(data?.num).toBe(123); + }); + + test("handles errors when adding to restricted collection", async () => { + const restrictedCollectionRef = collection( + firestore, + "restrictedCollection" + ); + const testData = { foo: "bar" }; + + const { result } = renderHook( + () => useAddDocumentMutation(restrictedCollectionRef, testData), + { wrapper } + ); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expectFirestoreError(result.current.error, "permission-denied"); + }); + + test("calls onSuccess callback with document reference", async () => { + const collectionRef = collection(firestore, "tests"); + const testData = { foo: "success" }; + let callbackDocRef: DocumentReference | null = null; + + const { result } = renderHook( + () => + useAddDocumentMutation(collectionRef, testData, { + onSuccess: (docRef) => { + callbackDocRef = docRef; + }, + }), + { wrapper } + ); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(callbackDocRef).toBeDefined(); + const snapshot = await getDoc(callbackDocRef!); + expect(snapshot.data()?.foo).toBe("success"); + }); + + test("handles empty data object", async () => { + const collectionRef = collection(firestore, "tests"); + const emptyData = {}; + + const { result } = renderHook( + () => useAddDocumentMutation(collectionRef, emptyData), + { wrapper } + ); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = await getDoc(result.current.data!); + expect(snapshot.exists()).toBe(true); + expect(snapshot.data()).toEqual({}); + }); +}); diff --git a/packages/react/src/firestore/useAddDocumentMutation.ts b/packages/react/src/firestore/useAddDocumentMutation.ts new file mode 100644 index 00000000..3b78550e --- /dev/null +++ b/packages/react/src/firestore/useAddDocumentMutation.ts @@ -0,0 +1,33 @@ +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { + type DocumentReference, + type FirestoreError, + type WithFieldValue, + type CollectionReference, + type DocumentData, + addDoc, +} from "firebase/firestore"; + +type FirestoreUseMutationOptions = Omit< + UseMutationOptions, + "mutationFn" +>; +export function useAddDocumentMutation< + AppModelType extends DocumentData = DocumentData, + DbModelType extends DocumentData = DocumentData +>( + collectionRef: CollectionReference, + data: WithFieldValue, + options?: FirestoreUseMutationOptions< + DocumentReference, + FirestoreError + > +) { + return useMutation< + DocumentReference, + FirestoreError + >({ + ...options, + mutationFn: () => addDoc(collectionRef, data), + }); +} From 5ad18d9ba4fb5a2c39b4a4a9fbcff3b9116de314 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 11 Feb 2025 17:00:31 +0300 Subject: [PATCH 2/2] refactor: move variables to mutation args --- .../firestore/useAddDocumentMutation.test.tsx | 35 +++++++++---------- .../src/firestore/useAddDocumentMutation.ts | 18 ++++++---- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/packages/react/src/firestore/useAddDocumentMutation.test.tsx b/packages/react/src/firestore/useAddDocumentMutation.test.tsx index 319cf835..667a5234 100644 --- a/packages/react/src/firestore/useAddDocumentMutation.test.tsx +++ b/packages/react/src/firestore/useAddDocumentMutation.test.tsx @@ -26,14 +26,13 @@ describe("useAddDocumentMutation", () => { const collectionRef = collection(firestore, "tests"); const testData = { foo: "bar", num: 42 }; - const { result } = renderHook( - () => useAddDocumentMutation(collectionRef, testData), - { wrapper } - ); + const { result } = renderHook(() => useAddDocumentMutation(collectionRef), { + wrapper, + }); expect(result.current.isIdle).toBe(true); - await act(() => result.current.mutate()); + await act(() => result.current.mutate(testData)); await waitFor(() => { expect(result.current.isSuccess).toBe(true); @@ -59,12 +58,11 @@ describe("useAddDocumentMutation", () => { ) as CollectionReference; const testData: TestDoc = { foo: "test", num: 123 }; - const { result } = renderHook( - () => useAddDocumentMutation(collectionRef, testData), - { wrapper } - ); + const { result } = renderHook(() => useAddDocumentMutation(collectionRef), { + wrapper, + }); - await act(() => result.current.mutate()); + await act(() => result.current.mutate(testData)); await waitFor(() => { expect(result.current.isSuccess).toBe(true); @@ -84,11 +82,11 @@ describe("useAddDocumentMutation", () => { const testData = { foo: "bar" }; const { result } = renderHook( - () => useAddDocumentMutation(restrictedCollectionRef, testData), + () => useAddDocumentMutation(restrictedCollectionRef), { wrapper } ); - await act(() => result.current.mutate()); + await act(() => result.current.mutate(testData)); await waitFor(() => { expect(result.current.isError).toBe(true); @@ -104,7 +102,7 @@ describe("useAddDocumentMutation", () => { const { result } = renderHook( () => - useAddDocumentMutation(collectionRef, testData, { + useAddDocumentMutation(collectionRef, { onSuccess: (docRef) => { callbackDocRef = docRef; }, @@ -112,7 +110,7 @@ describe("useAddDocumentMutation", () => { { wrapper } ); - await act(() => result.current.mutate()); + await act(() => result.current.mutate(testData)); await waitFor(() => { expect(result.current.isSuccess).toBe(true); @@ -127,12 +125,11 @@ describe("useAddDocumentMutation", () => { const collectionRef = collection(firestore, "tests"); const emptyData = {}; - const { result } = renderHook( - () => useAddDocumentMutation(collectionRef, emptyData), - { wrapper } - ); + const { result } = renderHook(() => useAddDocumentMutation(collectionRef), { + wrapper, + }); - await act(() => result.current.mutate()); + await act(() => result.current.mutate(emptyData)); await waitFor(() => { expect(result.current.isSuccess).toBe(true); diff --git a/packages/react/src/firestore/useAddDocumentMutation.ts b/packages/react/src/firestore/useAddDocumentMutation.ts index 3b78550e..e0dac39e 100644 --- a/packages/react/src/firestore/useAddDocumentMutation.ts +++ b/packages/react/src/firestore/useAddDocumentMutation.ts @@ -8,26 +8,32 @@ import { addDoc, } from "firebase/firestore"; -type FirestoreUseMutationOptions = Omit< - UseMutationOptions, +type FirestoreUseMutationOptions< + TData = unknown, + TError = Error, + AppModelType extends DocumentData = DocumentData +> = Omit< + UseMutationOptions>, "mutationFn" >; + export function useAddDocumentMutation< AppModelType extends DocumentData = DocumentData, DbModelType extends DocumentData = DocumentData >( collectionRef: CollectionReference, - data: WithFieldValue, options?: FirestoreUseMutationOptions< DocumentReference, - FirestoreError + FirestoreError, + AppModelType > ) { return useMutation< DocumentReference, - FirestoreError + FirestoreError, + WithFieldValue >({ ...options, - mutationFn: () => addDoc(collectionRef, data), + mutationFn: (data) => addDoc(collectionRef, data), }); }