diff --git a/dataconnect-sdk/js/default-connector/README.md b/dataconnect-sdk/js/default-connector/README.md index c3d1ddb..0c66142 100644 --- a/dataconnect-sdk/js/default-connector/README.md +++ b/dataconnect-sdk/js/default-connector/README.md @@ -28,7 +28,7 @@ A connector is a collection of Queries and Mutations. One SDK is generated for e You can find more information about connectors in the [Data Connect documentation](https://firebase.google.com/docs/data-connect#how-does). ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig } from '@dataconnect/default-connector'; const dataConnect = getDataConnect(connectorConfig); @@ -41,7 +41,7 @@ To connect to the emulator, you can use the following code. You can also follow the emulator instructions from the [Data Connect documentation](https://firebase.google.com/docs/data-connect/web-sdk#instrument-clients). ```javascript -import { connectDataConnectEmulator, getDataConnect, DataConnect } from 'firebase/data-connect'; +import { connectDataConnectEmulator, getDataConnect } from 'firebase/data-connect'; import { connectorConfig } from '@dataconnect/default-connector'; const dataConnect = getDataConnect(connectorConfig); @@ -98,7 +98,7 @@ export interface ListMoviesData { ### Using `ListMovies`'s action shortcut function ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig, listMovies } from '@dataconnect/default-connector'; @@ -122,7 +122,7 @@ listMovies().then((response) => { ### Using `ListMovies`'s `QueryRef` function ```javascript -import { getDataConnect, DataConnect, executeQuery } from 'firebase/data-connect'; +import { getDataConnect, executeQuery } from 'firebase/data-connect'; import { connectorConfig, listMoviesRef } from '@dataconnect/default-connector'; @@ -185,7 +185,7 @@ export interface GetMovieByIdData { ### Using `GetMovieById`'s action shortcut function ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig, getMovieById, GetMovieByIdVariables } from '@dataconnect/default-connector'; // The `GetMovieById` query requires an argument of type `GetMovieByIdVariables`: @@ -215,7 +215,7 @@ getMovieById(getMovieByIdVars).then((response) => { ### Using `GetMovieById`'s `QueryRef` function ```javascript -import { getDataConnect, DataConnect, executeQuery } from 'firebase/data-connect'; +import { getDataConnect, executeQuery } from 'firebase/data-connect'; import { connectorConfig, getMovieByIdRef, GetMovieByIdVariables } from '@dataconnect/default-connector'; // The `GetMovieById` query requires an argument of type `GetMovieByIdVariables`: @@ -275,7 +275,7 @@ export interface GetMetaData { ### Using `GetMeta`'s action shortcut function ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig, getMeta } from '@dataconnect/default-connector'; @@ -299,7 +299,7 @@ getMeta().then((response) => { ### Using `GetMeta`'s `QueryRef` function ```javascript -import { getDataConnect, DataConnect, executeQuery } from 'firebase/data-connect'; +import { getDataConnect, executeQuery } from 'firebase/data-connect'; import { connectorConfig, getMetaRef } from '@dataconnect/default-connector'; @@ -374,7 +374,7 @@ export interface CreateMovieData { ### Using `CreateMovie`'s action shortcut function ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig, createMovie, CreateMovieVariables } from '@dataconnect/default-connector'; // The `CreateMovie` mutation requires an argument of type `CreateMovieVariables`: @@ -406,7 +406,7 @@ createMovie(createMovieVars).then((response) => { ### Using `CreateMovie`'s `MutationRef` function ```javascript -import { getDataConnect, DataConnect, executeMutation } from 'firebase/data-connect'; +import { getDataConnect, executeMutation } from 'firebase/data-connect'; import { connectorConfig, createMovieRef, CreateMovieVariables } from '@dataconnect/default-connector'; // The `CreateMovie` mutation requires an argument of type `CreateMovieVariables`: @@ -474,7 +474,7 @@ export interface UpsertMovieData { ### Using `UpsertMovie`'s action shortcut function ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig, upsertMovie, UpsertMovieVariables } from '@dataconnect/default-connector'; // The `UpsertMovie` mutation requires an argument of type `UpsertMovieVariables`: @@ -506,7 +506,7 @@ upsertMovie(upsertMovieVars).then((response) => { ### Using `UpsertMovie`'s `MutationRef` function ```javascript -import { getDataConnect, DataConnect, executeMutation } from 'firebase/data-connect'; +import { getDataConnect, executeMutation } from 'firebase/data-connect'; import { connectorConfig, upsertMovieRef, UpsertMovieVariables } from '@dataconnect/default-connector'; // The `UpsertMovie` mutation requires an argument of type `UpsertMovieVariables`: @@ -572,7 +572,7 @@ export interface DeleteMovieData { ### Using `DeleteMovie`'s action shortcut function ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig, deleteMovie, DeleteMovieVariables } from '@dataconnect/default-connector'; // The `DeleteMovie` mutation requires an argument of type `DeleteMovieVariables`: @@ -602,7 +602,7 @@ deleteMovie(deleteMovieVars).then((response) => { ### Using `DeleteMovie`'s `MutationRef` function ```javascript -import { getDataConnect, DataConnect, executeMutation } from 'firebase/data-connect'; +import { getDataConnect, executeMutation } from 'firebase/data-connect'; import { connectorConfig, deleteMovieRef, DeleteMovieVariables } from '@dataconnect/default-connector'; // The `DeleteMovie` mutation requires an argument of type `DeleteMovieVariables`: @@ -660,7 +660,7 @@ export interface AddMetaData { ### Using `AddMeta`'s action shortcut function ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig, addMeta } from '@dataconnect/default-connector'; @@ -684,7 +684,7 @@ addMeta().then((response) => { ### Using `AddMeta`'s `MutationRef` function ```javascript -import { getDataConnect, DataConnect, executeMutation } from 'firebase/data-connect'; +import { getDataConnect, executeMutation } from 'firebase/data-connect'; import { connectorConfig, addMetaRef } from '@dataconnect/default-connector'; @@ -742,7 +742,7 @@ export interface DeleteMetaData { ### Using `DeleteMeta`'s action shortcut function ```javascript -import { getDataConnect, DataConnect } from 'firebase/data-connect'; +import { getDataConnect } from 'firebase/data-connect'; import { connectorConfig, deleteMeta, DeleteMetaVariables } from '@dataconnect/default-connector'; // The `DeleteMeta` mutation requires an argument of type `DeleteMetaVariables`: @@ -772,7 +772,7 @@ deleteMeta(deleteMetaVars).then((response) => { ### Using `DeleteMeta`'s `MutationRef` function ```javascript -import { getDataConnect, DataConnect, executeMutation } from 'firebase/data-connect'; +import { getDataConnect, executeMutation } from 'firebase/data-connect'; import { connectorConfig, deleteMetaRef, DeleteMetaVariables } from '@dataconnect/default-connector'; // The `DeleteMeta` mutation requires an argument of type `DeleteMetaVariables`: diff --git a/dataconnect-sdk/js/default-connector/index.cjs.js b/dataconnect-sdk/js/default-connector/index.cjs.js index 6fa0628..84b7ed0 100644 --- a/dataconnect-sdk/js/default-connector/index.cjs.js +++ b/dataconnect-sdk/js/default-connector/index.cjs.js @@ -12,62 +12,77 @@ exports.createMovieRef = function createMovieRef(dcOrVars, vars) { dcInstance._useGeneratedSdk(); return mutationRef(dcInstance, 'CreateMovie', inputVars); } + exports.createMovie = function createMovie(dcOrVars, vars) { return executeMutation(createMovieRef(dcOrVars, vars)); }; + exports.upsertMovieRef = function upsertMovieRef(dcOrVars, vars) { const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); dcInstance._useGeneratedSdk(); return mutationRef(dcInstance, 'UpsertMovie', inputVars); } + exports.upsertMovie = function upsertMovie(dcOrVars, vars) { return executeMutation(upsertMovieRef(dcOrVars, vars)); }; + exports.deleteMovieRef = function deleteMovieRef(dcOrVars, vars) { const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); dcInstance._useGeneratedSdk(); return mutationRef(dcInstance, 'DeleteMovie', inputVars); } + exports.deleteMovie = function deleteMovie(dcOrVars, vars) { return executeMutation(deleteMovieRef(dcOrVars, vars)); }; + exports.addMetaRef = function addMetaRef(dc) { const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); dcInstance._useGeneratedSdk(); return mutationRef(dcInstance, 'AddMeta'); } + exports.addMeta = function addMeta(dc) { return executeMutation(addMetaRef(dc)); }; + exports.deleteMetaRef = function deleteMetaRef(dcOrVars, vars) { const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); dcInstance._useGeneratedSdk(); return mutationRef(dcInstance, 'DeleteMeta', inputVars); } + exports.deleteMeta = function deleteMeta(dcOrVars, vars) { return executeMutation(deleteMetaRef(dcOrVars, vars)); }; + exports.listMoviesRef = function listMoviesRef(dc) { const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); dcInstance._useGeneratedSdk(); return queryRef(dcInstance, 'ListMovies'); } + exports.listMovies = function listMovies(dc) { return executeQuery(listMoviesRef(dc)); }; + exports.getMovieByIdRef = function getMovieByIdRef(dcOrVars, vars) { const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true); dcInstance._useGeneratedSdk(); return queryRef(dcInstance, 'GetMovieById', inputVars); } + exports.getMovieById = function getMovieById(dcOrVars, vars) { return executeQuery(getMovieByIdRef(dcOrVars, vars)); }; + exports.getMetaRef = function getMetaRef(dc) { const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined); dcInstance._useGeneratedSdk(); return queryRef(dcInstance, 'GetMeta'); } + exports.getMeta = function getMeta(dc) { return executeQuery(getMetaRef(dc)); }; diff --git a/packages/react/package.json b/packages/react/package.json index cdbc12f..ffb9b87 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack-query-firebase/react", - "version": "1.0.7", + "version": "2.0.8", "description": "TanStack Query bindings for Firebase and React", "type": "module", "scripts": { diff --git a/packages/react/src/data-connect/useDataConnectQuery.test.tsx b/packages/react/src/data-connect/useDataConnectQuery.test.tsx index 3054bb1..dfe7c92 100644 --- a/packages/react/src/data-connect/useDataConnectQuery.test.tsx +++ b/packages/react/src/data-connect/useDataConnectQuery.test.tsx @@ -269,4 +269,44 @@ describe("useDataConnectQuery", () => { { id: movieId }, ]); }); + + test("fetches new data when variables change", async () => { + const movieData1 = { + title: "tanstack query firebase 1", + genre: "library 1", + imageUrl: "https://invertase.io/1", + }; + const createdMovie = await createMovie(movieData1); + const movieData2 = { + title: "tanstack query firebase 2", + genre: "library 2", + imageUrl: "https://invertase.io/2", + }; + const createdMovie2 = await createMovie(movieData2); + + const movieId = createdMovie?.data?.movie_insert?.id; + const movieId2 = createdMovie2?.data?.movie_insert?.id; + getMovieByIdRef({ id: movieId2 }); + const ref = getMovieByIdRef({ id: movieId }); + const { result } = renderHook(() => useDataConnectQuery(ref), { + wrapper, + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.movie?.title).toBe(movieData1?.title); + expect(result.current.data?.movie?.genre).toBe(movieData1?.genre); + expect(result.current.data?.movie?.imageUrl).toBe(movieData1?.imageUrl); + }); + await act(async () => { + ref.variables.id = createdMovie2.data.movie_insert.id; + result.current.refetch(); + }); + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data?.movie?.title).toBe(movieData2?.title); + expect(result.current.data?.movie?.genre).toBe(movieData2?.genre); + expect(result.current.data?.movie?.imageUrl).toBe(movieData2?.imageUrl); + }); + }); }); diff --git a/packages/react/src/data-connect/useDataConnectQuery.ts b/packages/react/src/data-connect/useDataConnectQuery.ts index de0198e..3270305 100644 --- a/packages/react/src/data-connect/useDataConnectQuery.ts +++ b/packages/react/src/data-connect/useDataConnectQuery.ts @@ -1,8 +1,4 @@ -import { - type InitialDataFunction, - type UseQueryOptions, - useQuery, -} from "@tanstack/react-query"; +import { type UseQueryOptions, useQuery } from "@tanstack/react-query"; import type { FirebaseError } from "firebase/app"; import { type CallerSdkType, @@ -11,17 +7,23 @@ import { type QueryResult, executeQuery, } from "firebase/data-connect"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import type { PartialBy } from "../../utils"; import type { QueryResultRequiredRef, UseDataConnectQueryResult, } from "./types"; +import { deepEqual } from "./utils"; export type useDataConnectQueryOptions< TData = object, TError = FirebaseError, > = PartialBy, "queryFn">, "queryKey">; +function getRef( + refOrResult: QueryRef | QueryResult, +): QueryRef { + return "ref" in refOrResult ? refOrResult.ref : refOrResult; +} export function useDataConnectQuery( refOrResult: QueryRef | QueryResult, @@ -31,17 +33,24 @@ export function useDataConnectQuery( const [dataConnectResult, setDataConnectResult] = useState< QueryResultRequiredRef >("ref" in refOrResult ? refOrResult : { ref: refOrResult }); + const [ref, setRef] = useState(dataConnectResult.ref); // TODO(mtewani): in the future we should allow for users to pass in `QueryResult` objects into `initialData`. - let initialData: Data | InitialDataFunction | undefined; - const { ref } = dataConnectResult; + const [initialData] = useState( + dataConnectResult.data || options?.initialData, + ); - if ("ref" in refOrResult) { - initialData = { - ...refOrResult.data, - }; - } else { - initialData = options?.initialData; - } + useEffect(() => { + setRef((oldRef) => { + const newRef = getRef(refOrResult); + if ( + newRef.name !== oldRef.name || + !deepEqual(oldRef.variables, newRef.variables) + ) { + return newRef; + } + return oldRef; + }); + }, [refOrResult]); // @ts-expect-error function is hidden under `DataConnect`. ref.dataConnect._setCallerSdkType(_callerSdkType); diff --git a/packages/react/src/data-connect/utils.test.ts b/packages/react/src/data-connect/utils.test.ts new file mode 100644 index 0000000..959fd81 --- /dev/null +++ b/packages/react/src/data-connect/utils.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "vitest"; +import { deepEqual } from "./utils"; + +describe("utils", () => { + test("should compare native types correctly", () => { + const nonEqualNumbers = deepEqual(1, 2); + expect(nonEqualNumbers).to.eq(false); + const equalNumbers = deepEqual(2, 2); + expect(equalNumbers).to.eq(true); + const nonEqualBools = deepEqual(false, true); + expect(nonEqualBools).to.eq(false); + const equalBools = deepEqual(false, false); + expect(equalBools).to.eq(true); + const nonEqualStrings = deepEqual("a", "b"); + expect(nonEqualStrings).to.eq(false); + const equalStrings = deepEqual("a", "a"); + expect(equalStrings).to.eq(true); + }); + test("should compare object types correctly", () => { + const equalNulls = deepEqual(null, null); + expect(equalNulls).to.eq(true); + const nonEqualNulls = deepEqual(null, {}); + expect(nonEqualNulls).to.eq(false); + const nonEqualObjects = deepEqual({}, { a: "b" }); + expect(nonEqualObjects).to.eq(false); + const equalObjects = deepEqual({ a: "b" }, { a: "b" }); + expect(equalObjects).to.eq(true); + const equalObjectsWtesthOrder = deepEqual( + { a: "b", b: "c" }, + { b: "c", a: "b" }, + ); + expect(equalObjectsWtesthOrder).to.eq(true); + const nestedObjects = deepEqual( + { a: { movie_insert: "b" } }, + { a: { movie_insert: "c" } }, + ); + expect(nestedObjects).to.eq(false); + }); + test("should compare arrays correctly", () => { + const emptyArrays = deepEqual([], []); + expect(emptyArrays).to.eq(true); + const nonEmptyArrays = deepEqual([1], [1]); + expect(nonEmptyArrays).to.eq(true); + const nonEmptyDiffArrays = deepEqual([2], [1]); + expect(nonEmptyDiffArrays).to.eq(false); + }); +}); diff --git a/packages/react/src/data-connect/utils.ts b/packages/react/src/data-connect/utils.ts new file mode 100644 index 0000000..c199548 --- /dev/null +++ b/packages/react/src/data-connect/utils.ts @@ -0,0 +1,39 @@ +export function deepEqual(a: unknown, b: unknown) { + if (typeof a !== typeof b) { + return false; + } + if (typeof a === "object" && a !== null) { + if (a === b) { + return true; + } + if (Array.isArray(a)) { + if (a.length !== (b as unknown[]).length) { + return false; + } + for (let index = 0; index < a.length; index++) { + const elementA = a[index]; + const elementB = (b as unknown[])[index]; + const isEqual = deepEqual(elementA, elementB); + if (!isEqual) { + return false; + } + } + return true; + } + const keys = Object.keys(a); + if (keys.length !== Object.keys(b as object).length) { + return false; + } + for (const key of keys) { + const isEqual = deepEqual( + a[key as keyof typeof a], + (b as object)[key as keyof typeof b], + ); + if (!isEqual) { + return false; + } + } + return true; + } + return a === b; +}