Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react/src/firestore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export { useDocumentQuery } from "./useDocumentQuery";
export { useCollectionQuery } from "./useCollectionQuery";
export { useGetAggregateFromServerQuery } from "./useGetAggregateFromServerQuery";
export { useGetCountFromServerQuery } from "./useGetCountFromServerQuery";
export { useUpdateDocumentMutation } from "./useUpdateDocumentMutation";
export { useNamedQuery } from "./useNamedQuery";
export { useDeleteDocumentMutation } from "./useDeleteDocumentMutation";
197 changes: 197 additions & 0 deletions packages/react/src/firestore/useUpdateDocumentMutation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
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 { useUpdateDocumentMutation } from "./useUpdateDocumentMutation";

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

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

test("successfully updates an existing document", async () => {
const docRef = doc(firestore, "tests", "updateTest");

await setDoc(docRef, { foo: "initial", num: 1, unchanged: "same" });

const updateData = { foo: "updated", num: 2 };

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

expect(result.current.isPending).toBe(false);

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

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

const snapshot = await getDoc(docRef);
expect(snapshot.exists()).toBe(true);
expect(snapshot.data()).toEqual({
foo: "updated",
num: 2,
unchanged: "same",
});
});

test("handles nested field updates", async () => {
const docRef = doc(firestore, "tests", "nestedTest");

await setDoc(docRef, {
nested: { field1: "old", field2: "keep" },
top: "unchanged",
});

const updateData = {
"nested.field1": "new",
};

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

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

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

const snapshot = await getDoc(docRef);
const data = snapshot.data();
expect(data?.nested.field1).toBe("new");
expect(data?.nested.field2).toBe("keep");
expect(data?.top).toBe("unchanged");
});

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

const docRef = doc(
firestore,
"tests",
"typedDoc"
) as DocumentReference<TestDoc>;

await setDoc(docRef, { foo: "initial", num: 1 });

const updateData = { num: 42 };

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

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

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

const snapshot = await getDoc(docRef);
const data = snapshot.data();
expect(data?.foo).toBe("initial"); // unchanged
expect(data?.num).toBe(42); // updated
});

test("fails when updating non-existent document", async () => {
const nonExistentDocRef = doc(firestore, "tests", "doesNotExist");
const updateData = { foo: "bar" };

const { result } = renderHook(
() => useUpdateDocumentMutation(nonExistentDocRef),
{ wrapper }
);

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

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

expectFirestoreError(result.current.error, "not-found");
});

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

const { result } = renderHook(
() => useUpdateDocumentMutation(restrictedDocRef),
{ wrapper }
);

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

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

expectFirestoreError(result.current.error, "permission-denied");
});

test("calls onSuccess callback after update", async () => {
const docRef = doc(firestore, "tests", "callbackTest");
await setDoc(docRef, { foo: "initial" });

let callbackCalled = false;
const updateData = { foo: "updated" };

const { result } = renderHook(
() =>
useUpdateDocumentMutation(docRef, {
onSuccess: () => {
callbackCalled = true;
},
}),
{ wrapper }
);

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

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

expect(callbackCalled).toBe(true);
const snapshot = await getDoc(docRef);
expect(snapshot.data()?.foo).toBe("updated");
});

test("handles empty update object", async () => {
const docRef = doc(firestore, "tests", "emptyUpdateTest");
await setDoc(docRef, { foo: "initial" });

const emptyUpdate = {};

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

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

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

const snapshot = await getDoc(docRef);
expect(snapshot.data()).toEqual({ foo: "initial" });
});
});
30 changes: 30 additions & 0 deletions packages/react/src/firestore/useUpdateDocumentMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
import {
type DocumentReference,
type FirestoreError,
type DocumentData,
updateDoc,
type UpdateData,
} from "firebase/firestore";

type FirestoreUseMutationOptions<
TData = unknown,
TError = Error,
DbModelType extends DocumentData = DocumentData
> = Omit<
UseMutationOptions<TData, TError, UpdateData<DbModelType>>,
"mutationFn"
>;

export function useUpdateDocumentMutation<
AppModelType extends DocumentData = DocumentData,
DbModelType extends DocumentData = DocumentData
>(
documentRef: DocumentReference<AppModelType, DbModelType>,
options?: FirestoreUseMutationOptions<void, FirestoreError, DbModelType>
) {
return useMutation<void, FirestoreError, UpdateData<DbModelType>>({
...options,
mutationFn: (data) => updateDoc(documentRef, data),
});
}