Skip to content

Commit

Permalink
feat(react/firestore): add useSetDocumentMutation
Browse files Browse the repository at this point in the history
  • Loading branch information
HassanBahati committed Jan 31, 2025
1 parent 5e6e46a commit 8366b3c
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/react/src/firestore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { useCollectionQuery } from "./useCollectionQuery";
export { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery";
export { useGetCountFromServerQuery } from "./useGetCountFromServerQuery";
// useNamedQuery
export { useSetDocumentMutation } from "./useSetDocumentMutation";
169 changes: 169 additions & 0 deletions packages/react/src/firestore/useSetDocumentMutation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { renderHook, waitFor, act } from "@testing-library/react";
import { doc, type DocumentReference, getDoc } from "firebase/firestore";
import { beforeEach, describe, expect, test } from "vitest";
import { useSetDocumentMutation } from "./useSetDocumentMutation";

import {
expectFirestoreError,
firestore,
wipeFirestore,
} from "~/testing-utils";
import { queryClient, wrapper } from "../../utils";

describe("useSetDocumentMutation", () => {
beforeEach(async () => {
await wipeFirestore();
queryClient.clear();
});

test("successfully sets a new document", async () => {
const docRef = doc(firestore, "tests", "setTest");
const testData = { foo: "bar", num: 42 };

const { result } = renderHook(
() => useSetDocumentMutation(docRef, testData),
{ 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 snapshot = await getDoc(docRef);
expect(snapshot.exists()).toBe(true);
expect(snapshot.data()).toEqual(testData);
});

test("successfully overwrites existing document", async () => {
const docRef = doc(firestore, "tests", "overwriteTest");
const initialData = { foo: "initial", num: 1 };
const newData = { foo: "updated", num: 2 };

const { result } = renderHook(
() => useSetDocumentMutation(docRef, initialData),
{ wrapper }
);

await act(() => result.current.mutate());

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

let snapshot = await getDoc(docRef);
expect(snapshot.data()).toEqual(initialData);

const { result: result2 } = renderHook(
() => useSetDocumentMutation(docRef, newData),
{ wrapper }
);

await act(() => result2.current.mutate());

await waitFor(() => {
expect(result2.current.isSuccess).toBe(true);
});

snapshot = await getDoc(docRef);
expect(snapshot.data()).toEqual(newData);
});

test("handles type-safe document data", async () => {
interface TestDoc {
foo: string;
num: number;
}

const docRef = doc(
firestore,
"tests",
"typedDoc"
) as DocumentReference<TestDoc>;
const testData: TestDoc = { foo: "test", num: 123 };

const { result } = renderHook(
() => useSetDocumentMutation(docRef, testData),
{ wrapper }
);

await act(() => result.current.mutate());

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

const snapshot = await getDoc(docRef);
const data = snapshot.data();
expect(data?.foo).toBe("test");
expect(data?.num).toBe(123);
});

test("handles errors when setting to restricted collection", async () => {
const restrictedDocRef = doc(firestore, "restrictedCollection", "someDoc");
const testData = { foo: "bar" };

const { result } = renderHook(
() => useSetDocumentMutation(restrictedDocRef, 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 after setting document", async () => {
const docRef = doc(firestore, "tests", "callbackTest");
const testData = { foo: "callback" };
let callbackCalled = false;

const { result } = renderHook(
() =>
useSetDocumentMutation(docRef, testData, {
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.data()?.foo).toBe("callback");
});

test("handles empty data object", async () => {
const docRef = doc(firestore, "tests", "emptyDoc");
const emptyData = {};

const { result } = renderHook(
() => useSetDocumentMutation(docRef, emptyData),
{ wrapper }
);

await act(() => result.current.mutate());

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

const snapshot = await getDoc(docRef);
expect(snapshot.exists()).toBe(true);
expect(snapshot.data()).toEqual({});
});
});
26 changes: 26 additions & 0 deletions packages/react/src/firestore/useSetDocumentMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
import {
type DocumentReference,
type FirestoreError,
type WithFieldValue,
type DocumentData,
setDoc,
} from "firebase/firestore";

type FirestoreUseMutationOptions<TData = unknown, TError = Error> = Omit<
UseMutationOptions<TData, TError, void>,
"mutationFn"
>;
export function useSetDocumentMutation<
AppModelType extends DocumentData = DocumentData,
DbModelType extends DocumentData = DocumentData
>(
documentRef: DocumentReference<AppModelType, DbModelType>,
data: WithFieldValue<AppModelType>,
options?: FirestoreUseMutationOptions<void, FirestoreError>
) {
return useMutation<void, FirestoreError>({
...options,
mutationFn: () => setDoc(documentRef, data),
});
}

0 comments on commit 8366b3c

Please sign in to comment.