From 63f569fa8a74a455e44e35c6f814c54cd7009cfd Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Fri, 31 Jan 2025 05:41:24 +0300 Subject: [PATCH] feat(react/firestore): add useDeleteDocumentMutation --- packages/react/src/firestore/index.ts | 1 + .../useDeleteDocumentMutation.test.tsx | 137 ++++++++++++++++++ .../firestore/useDeleteDocumentMutation.ts | 25 ++++ 3 files changed, 163 insertions(+) create mode 100644 packages/react/src/firestore/useDeleteDocumentMutation.test.tsx create mode 100644 packages/react/src/firestore/useDeleteDocumentMutation.ts diff --git a/packages/react/src/firestore/index.ts b/packages/react/src/firestore/index.ts index ec75c7b1..2bf7662b 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 { useDeleteDocumentMutation } from "./useDeleteDocumentMutation"; diff --git a/packages/react/src/firestore/useDeleteDocumentMutation.test.tsx b/packages/react/src/firestore/useDeleteDocumentMutation.test.tsx new file mode 100644 index 00000000..2c31bf2b --- /dev/null +++ b/packages/react/src/firestore/useDeleteDocumentMutation.test.tsx @@ -0,0 +1,137 @@ +import { renderHook, waitFor, act } from "@testing-library/react"; +import { + doc, + type DocumentReference, + getDoc, + setDoc, +} from "firebase/firestore"; +import { beforeEach, describe, expect, test } from "vitest"; +import { useDeleteDocumentMutation } from "./useDeleteDocumentMutation"; + +import { + expectFirestoreError, + firestore, + wipeFirestore, +} from "~/testing-utils"; +import { queryClient, wrapper } from "../../utils"; + +describe("useDeleteDocumentMutation", () => { + beforeEach(async () => { + await wipeFirestore(); + queryClient.clear(); + }); + + test("successfully deletes an existing document", async () => { + const docRef = doc(firestore, "tests", "deleteTest"); + + await setDoc(docRef, { foo: "bar" }); + + const initialSnapshot = await getDoc(docRef); + expect(initialSnapshot.exists()).toBe(true); + + const { result } = renderHook(() => useDeleteDocumentMutation(docRef), { + wrapper, + }); + + expect(result.current.isPending).toBe(false); + expect(result.current.isIdle).toBe(true); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const finalSnapshot = await getDoc(docRef); + expect(finalSnapshot.exists()).toBe(false); + }); + + test("handles type-safe document references", async () => { + interface TestDoc { + foo: string; + num: number; + } + + const docRef = doc( + firestore, + "tests", + "typedDoc" + ) as DocumentReference; + await setDoc(docRef, { foo: "test", num: 123 }); + + const { result } = renderHook(() => useDeleteDocumentMutation(docRef), { + wrapper, + }); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = await getDoc(docRef); + expect(snapshot.exists()).toBe(false); + }); + + test("handles errors when deleting from restricted collection", async () => { + const restrictedDocRef = doc(firestore, "restrictedCollection", "someDoc"); + + const { result } = renderHook( + () => useDeleteDocumentMutation(restrictedDocRef), + { wrapper } + ); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expectFirestoreError(result.current.error, "permission-denied"); + }); + + test("calls onSuccess callback after deletion", async () => { + const docRef = doc(firestore, "tests", "callbackTest"); + await setDoc(docRef, { foo: "callback" }); + + let callbackCalled = false; + + const { result } = renderHook( + () => + useDeleteDocumentMutation(docRef, { + onSuccess: () => { + callbackCalled = true; + }, + }), + { wrapper } + ); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(callbackCalled).toBe(true); + const snapshot = await getDoc(docRef); + expect(snapshot.exists()).toBe(false); + }); + + test("handles deletion of non-existent document", async () => { + const nonExistentDocRef = doc(firestore, "tests", "doesNotExist"); + + const { result } = renderHook( + () => useDeleteDocumentMutation(nonExistentDocRef), + { wrapper } + ); + + await act(() => result.current.mutate()); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + const snapshot = await getDoc(nonExistentDocRef); + expect(snapshot.exists()).toBe(false); + }); +}); diff --git a/packages/react/src/firestore/useDeleteDocumentMutation.ts b/packages/react/src/firestore/useDeleteDocumentMutation.ts new file mode 100644 index 00000000..e5794804 --- /dev/null +++ b/packages/react/src/firestore/useDeleteDocumentMutation.ts @@ -0,0 +1,25 @@ +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { + deleteDoc, + type FirestoreError, + type DocumentData, + type DocumentReference, +} from "firebase/firestore"; + +type FirestoreUseMutationOptions = Omit< + UseMutationOptions, + "mutationFn" +>; + +export function useDeleteDocumentMutation< + AppModelType extends DocumentData = DocumentData, + DbModelType extends DocumentData = DocumentData +>( + documentRef: DocumentReference, + options?: FirestoreUseMutationOptions +) { + return useMutation({ + ...options, + mutationFn: () => deleteDoc(documentRef), + }); +}