From 1b02a0018662ceb975ef944339e9e2355b8f1bdb Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Tue, 17 Dec 2024 21:08:18 +0000 Subject: [PATCH] feat: Support flattened result structure --- examples/react-example/src/firebase.ts | 2 +- packages/react/package.json | 4 +- packages/react/src/data-connect/index.ts | 1 + .../react/src/data-connect/query-client.ts | 70 +++++++++++++++++++ packages/react/src/data-connect/types.ts | 15 ++++ .../src/data-connect/useConnectMutation.ts | 45 +++++++++--- .../react/src/data-connect/useConnectQuery.ts | 38 ++++++---- 7 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 packages/react/src/data-connect/query-client.ts create mode 100644 packages/react/src/data-connect/types.ts diff --git a/examples/react-example/src/firebase.ts b/examples/react-example/src/firebase.ts index 5c29966..9c9740b 100644 --- a/examples/react-example/src/firebase.ts +++ b/examples/react-example/src/firebase.ts @@ -7,5 +7,5 @@ if (getApps().length === 0) { projectId: "example", }); const dataConnect = getDataConnect(connectorConfig); - connectDataConnectEmulator(dataConnect!, "localhost", 5003); + connectDataConnectEmulator(dataConnect!, "localhost", 9399); } \ No newline at end of file diff --git a/packages/react/package.json b/packages/react/package.json index 06273fd..333e398 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -32,8 +32,8 @@ "license": "Apache-2.0", "devDependencies": { "@testing-library/react": "^16.0.1", - "react": "^18.3.1", - "@types/react": "^18.3.5", + "react": "^19.0.0", + "@types/react": "^19.0.1", "@dataconnect/default-connector": "workspace:*" }, "peerDependencies": { diff --git a/packages/react/src/data-connect/index.ts b/packages/react/src/data-connect/index.ts index 16b2977..2a6b8d9 100644 --- a/packages/react/src/data-connect/index.ts +++ b/packages/react/src/data-connect/index.ts @@ -1,2 +1,3 @@ +export { DataConnectQueryClient } from "./query-client"; export { useConnectQuery } from "./useConnectQuery"; export { useConnectMutation } from "./useConnectMutation"; diff --git a/packages/react/src/data-connect/query-client.ts b/packages/react/src/data-connect/query-client.ts new file mode 100644 index 0000000..c5cf599 --- /dev/null +++ b/packages/react/src/data-connect/query-client.ts @@ -0,0 +1,70 @@ +import { + QueryClient, + QueryKey, + type FetchQueryOptions, +} from "@tanstack/react-query"; +import type { FirebaseError } from "firebase/app"; +import type { FlattenedQueryResult } from "./types"; +import { executeQuery, QueryRef, QueryResult } from "firebase/data-connect"; + +type DataConnectQueryOptions = Omit< + FetchQueryOptions< + FlattenedQueryResult, + FirebaseError, + FlattenedQueryResult, + QueryKey + >, + "queryFn" | "queryKey" +> & { + queryRef: QueryRef; + queryKey?: QueryKey; +}; + +export class DataConnectQueryClient extends QueryClient { + prefetchDataConnectQuery, Variables>( + refOrResult: QueryRef | QueryResult, + options?: DataConnectQueryOptions + ) { + let queryRef: QueryRef; + let initialData: FlattenedQueryResult | undefined; + + if ("ref" in refOrResult) { + queryRef = refOrResult.ref; + initialData = { + ...refOrResult.data, + ref: refOrResult.ref, + source: refOrResult.source, + fetchTime: refOrResult.fetchTime, + }; + } else { + queryRef = refOrResult; + } + + return this.prefetchQuery< + FlattenedQueryResult, + FirebaseError, + FlattenedQueryResult, + QueryKey + >({ + ...options, + initialData, + queryKey: options?.queryKey ?? [ + queryRef.name, + queryRef.variables || null, + ], + queryFn: async () => { + const response = await executeQuery(queryRef); + + const data = { + ...response.data, + ref: response.ref, + source: response.source, + fetchTime: response.fetchTime, + }; + + // Ensures no serialization issues with undefined values + return JSON.parse(JSON.stringify(data)); + }, + }); + } +} diff --git a/packages/react/src/data-connect/types.ts b/packages/react/src/data-connect/types.ts new file mode 100644 index 0000000..3b7e7fd --- /dev/null +++ b/packages/react/src/data-connect/types.ts @@ -0,0 +1,15 @@ +import { MutationResult, QueryResult } from "firebase/data-connect"; + +// Flattens a QueryResult data down into a single object. +// This is to prevent query.data.data, and also expose additional properties. +export type FlattenedQueryResult = Omit< + QueryResult, + "data" | "toJSON" +> & + Data; + +export type FlattenedMutationResult = Omit< + MutationResult, + "data" | "toJSON" +> & + Data; diff --git a/packages/react/src/data-connect/useConnectMutation.ts b/packages/react/src/data-connect/useConnectMutation.ts index b3a9ced..5e7a2b2 100644 --- a/packages/react/src/data-connect/useConnectMutation.ts +++ b/packages/react/src/data-connect/useConnectMutation.ts @@ -2,7 +2,6 @@ import { useMutation, useQueryClient, type UseMutationOptions, - type UseMutationResult, } from "@tanstack/react-query"; import { type FirebaseError } from "firebase/app"; import { @@ -11,13 +10,16 @@ import { type DataConnect, type QueryRef, } from "firebase/data-connect"; +import { FlattenedMutationResult } from "./types"; type UseConnectMutationOptions< TData = unknown, TError = FirebaseError, Variables = unknown > = Omit, "mutationFn"> & { - invalidate?: QueryRef[]; + invalidate?: Array< + QueryRef | (() => QueryRef) + >; }; export function useConnectMutation< @@ -33,26 +35,47 @@ export function useConnectMutation< : never >( ref: Fn, - options?: UseConnectMutationOptions -): UseMutationResult { + options?: UseConnectMutationOptions< + FlattenedMutationResult, + FirebaseError, + Variables + > +) { const queryClient = useQueryClient(); - return useMutation({ + return useMutation< + FlattenedMutationResult, + FirebaseError, + Variables + >({ ...options, onSuccess(...args) { if (options?.invalidate && options.invalidate.length) { for (const ref of options.invalidate) { - queryClient.invalidateQueries({ - queryKey: [ref.name, ref.variables], - }); + if ("variables" in ref) { + queryClient.invalidateQueries({ + queryKey: [ref.name, ref.variables || null], + exact: true, + }); + } else { + queryClient.invalidateQueries({ + queryKey: [ref().name], + }); + } } } options?.onSuccess?.(...args); }, - mutationFn: async (variables: Variables) => { - const { data } = await executeMutation(ref(variables)); - return data; + mutationFn: async (variables) => { + const response = await executeMutation(ref(variables)); + + return { + ...response.data, + ref: response.ref, + source: response.source, + fetchTime: response.fetchTime, + }; }, }); } diff --git a/packages/react/src/data-connect/useConnectQuery.ts b/packages/react/src/data-connect/useConnectQuery.ts index b9b609b..1445404 100644 --- a/packages/react/src/data-connect/useConnectQuery.ts +++ b/packages/react/src/data-connect/useConnectQuery.ts @@ -1,6 +1,7 @@ import { useQuery, type UseQueryOptions } from "@tanstack/react-query"; -import { type FirebaseError } from "firebase/app"; +import type { FirebaseError } from "firebase/app"; import type { PartialBy } from "../../utils"; +import type { FlattenedQueryResult } from "./types"; import { type QueryRef, type QueryResult, @@ -12,30 +13,41 @@ type UseConnectQueryOptions< TError = FirebaseError > = PartialBy, "queryFn">, "queryKey">; -export function useConnectQuery< - Data extends Record, - Variables = unknown ->( +export function useConnectQuery( refOrResult: QueryRef | QueryResult, - options?: UseConnectQueryOptions + options?: UseConnectQueryOptions< + FlattenedQueryResult, + FirebaseError + > ) { let queryRef: QueryRef; - let initialData: Data | undefined; + let initialData: FlattenedQueryResult | undefined; if ("ref" in refOrResult) { queryRef = refOrResult.ref; - initialData = refOrResult.data; + initialData = { + ...refOrResult.data, + ref: refOrResult.ref, + source: refOrResult.source, + fetchTime: refOrResult.fetchTime, + }; } else { queryRef = refOrResult; } - return useQuery({ - initialData, + return useQuery, FirebaseError>({ ...options, - queryKey: options?.queryKey ?? [queryRef.name, queryRef.variables], + initialData, + queryKey: options?.queryKey ?? [queryRef.name, queryRef.variables || null], queryFn: async () => { - const { data } = await executeQuery(queryRef); - return data; + const response = await executeQuery(queryRef); + + return { + ...response.data, + ref: response.ref, + source: response.source, + fetchTime: response.fetchTime, + }; }, }); }