diff --git a/packages/angular/src/data-connect/index.ts b/packages/angular/src/data-connect/index.ts new file mode 100644 index 0000000..fdc9668 --- /dev/null +++ b/packages/angular/src/data-connect/index.ts @@ -0,0 +1,216 @@ +import { + CreateMutationOptions, + CreateMutationResult, + CreateQueryOptions, + CreateQueryResult, + injectMutation, + injectQuery, + QueryClient, + QueryKey, +} from "@tanstack/angular-query-experimental"; +import { + DataConnect, + executeMutation, + executeQuery, + MutationRef, + MutationResult, + QueryRef, + QueryResult, +} from "firebase/data-connect"; +import { FirebaseError } from "firebase/app"; + +import { inject, signal } from "@angular/core"; +function getQueryKey(queryRef: QueryRef) { + return [queryRef.name, queryRef.variables]; +} +type FlattenedQueryResult = Omit< + QueryResult, + "data" | "toJSON" +> & + Data; +interface CreateDataConnectQueryOptions + extends Omit< + CreateQueryOptions< + FlattenedQueryResult, + FirebaseError, + FlattenedQueryResult, + QueryKey + >, + "queryFn" | "queryKey" + > { + queryFn: () => QueryRef; +} + +/** + * injectDataConnectQuery takes a query ref and returns a wrapper function around Tanstack's `injectQuery` + * @param queryRefOrOptionsFn Query Ref or callback function for calling a new query + * @returns {CreateQueryResult>} + */ +export function injectDataConnectQuery( + queryRefOrOptionsFn: + | QueryRef + | (() => CreateDataConnectQueryOptions) +): CreateQueryResult, FirebaseError> { + const queryKey = signal([]); + function fdcOptionsFn() { + const passedInOptions = + typeof queryRefOrOptionsFn === "function" + ? queryRefOrOptionsFn() + : undefined; + + const modifiedFn = () => { + const ref: QueryRef = + passedInOptions?.queryFn() || + (queryRefOrOptionsFn as QueryRef); + queryKey.set([ref.name, ref.variables]); + return executeQuery(ref).then((res) => { + const { data, ...rest } = res; + return { + ...data, + ...rest, + }; + }) as Promise>; + }; + return { + queryKey: queryKey(), + ...passedInOptions, + queryFn: modifiedFn, + }; + } + return injectQuery(fdcOptionsFn); +} + +export type GeneratedSignature = ( + dc: DataConnect, + vars: Variables +) => MutationRef; +export type DataConnectMutationOptionsFn = + () => CreateDataConnectMutationOptions; +export type DataConnectMutationOptionsUndefinedMutationFn< + Data, + Error, + Variables +> = () => Omit< + ReturnType>, + "mutationFn" +>; +export type FlattenedMutationResult = Omit< + MutationResult, + "data" | "toJSON" +> & + Data; + + export interface CreateDataConnectMutationOptions extends Omit, "mutationFn"> { + invalidate?: QueryKey | QueryRef[]; + dataConnect?: DataConnect; + mutationFn: (args: Arguments) => MutationRef; +}; + +export function injectDataConnectMutation( + factoryFn: undefined, + optionsFn: DataConnectMutationOptionsFn< + Data, + FirebaseError, + Variables, + Arguments + > +): CreateMutationResult< + FlattenedMutationResult, + FirebaseError, + Arguments +>; +export function injectDataConnectMutation< + Data, + Variables extends undefined, + Arguments = void | undefined +>( + factoryFn: GeneratedSignature +): CreateMutationResult< + FlattenedMutationResult, + FirebaseError, + Arguments +>; +export function injectDataConnectMutation( + factoryFn: GeneratedSignature +): CreateMutationResult< + FlattenedMutationResult, + FirebaseError, + Arguments +>; +export function injectDataConnectMutation( + factoryFn: GeneratedSignature, + optionsFn?: DataConnectMutationOptionsUndefinedMutationFn< + Data, + FirebaseError, + Arguments + > +): CreateMutationResult< + FlattenedMutationResult, + FirebaseError, + Arguments +>; +/** + * injectDataConnectMutation takes a mutation ref factory function and returns a tanstack wrapper around `injectMutation` + * @param factoryFn generated SDK factory function + * @param optionsFn options function to create a new mutation + * @returns {CreateMutationResult, FirebaseError, Arguments>} + */ +export function injectDataConnectMutation< + Data, + Variables, + Arguments extends Variables +>( + factoryFn: GeneratedSignature | undefined, + optionsFn?: + | DataConnectMutationOptionsFn + | DataConnectMutationOptionsUndefinedMutationFn< + Data, + FirebaseError, + Variables + > +): CreateMutationResult< + FlattenedMutationResult, + FirebaseError, + Arguments +> { + const injectCb = () => { + const queryClient = inject(QueryClient); + const providedOptions = optionsFn ? optionsFn() : undefined; + const modifiedFn = (args: Arguments) => { + const dataConnect = inject(DataConnect); + const ref = + (providedOptions && + "mutationFn" in providedOptions && + providedOptions.mutationFn(args as Arguments)) || + factoryFn!(dataConnect, args as Variables); + + return executeMutation(ref) + .then((res) => { + const { data, ...rest } = res; + return { + ...data, + ...rest, + }; + }) + .then((ret) => { + providedOptions?.invalidate?.forEach((qk) => { + let key = qk; + if ("name" in (qk as Object)) { + const queryKey = getQueryKey(qk as QueryRef); + key = queryKey; + } + queryClient.invalidateQueries({ + queryKey: key, + }); + }); + return ret; + }) as Promise>; + }; + + return { + ...providedOptions, + mutationFn: modifiedFn, + }; + }; + return injectMutation(injectCb); +} diff --git a/packages/react/src/data-connect/types.ts b/packages/react/src/data-connect/types.ts index 1abd2e4..10d78f8 100644 --- a/packages/react/src/data-connect/types.ts +++ b/packages/react/src/data-connect/types.ts @@ -13,3 +13,12 @@ export type FlattenedMutationResult = Omit< "data" | "toJSON" > & Data; + +export enum CallerSdkType { + Base, // Core JS SDK + Generated, // Generated JS SDK + TanstackReactCore, // Tanstack non-generated React SDK + GeneratedReact, // Generated React SDK + TanstackAngularCore, // Tanstack non-generated Angular SDK + GeneratedAngular // Generated Angular SDK +} \ No newline at end of file diff --git a/packages/react/src/data-connect/useDataConnectMutation.ts b/packages/react/src/data-connect/useDataConnectMutation.ts index d277d4c..19c522c 100644 --- a/packages/react/src/data-connect/useDataConnectMutation.ts +++ b/packages/react/src/data-connect/useDataConnectMutation.ts @@ -10,7 +10,7 @@ import { type QueryRef, executeMutation, } from "firebase/data-connect"; -import type { FlattenedMutationResult } from "./types"; +import { CallerSdkType, type FlattenedMutationResult } from "./types"; export type useDataConnectMutationOptions< TData = unknown, @@ -48,10 +48,10 @@ export function useDataConnectMutation< FlattenedMutationResult, FirebaseError, Variables - > + >, + _callerSdkType: CallerSdkType = CallerSdkType.TanstackReactCore ) { const queryClient = useQueryClient(); - return useMutation< FlattenedMutationResult, FirebaseError, @@ -78,6 +78,9 @@ export function useDataConnectMutation< }, mutationFn: async (variables) => { const mutationRef = typeof ref === "function" ? ref(variables) : ref; + + // @ts-expect-error function is hidden under `DataConnect`. + mutationRef.dataConnect._setCallerSdkType(_callerSdkType); const response = await executeMutation(mutationRef); return { diff --git a/packages/react/src/data-connect/useDataConnectQuery.ts b/packages/react/src/data-connect/useDataConnectQuery.ts index 31ca7bb..95883ec 100644 --- a/packages/react/src/data-connect/useDataConnectQuery.ts +++ b/packages/react/src/data-connect/useDataConnectQuery.ts @@ -6,7 +6,8 @@ import { executeQuery, } from "firebase/data-connect"; import type { PartialBy } from "../../utils"; -import type { FlattenedQueryResult } from "./types"; +import { CallerSdkType, type FlattenedQueryResult } from "./types"; + export type useDataConnectQueryOptions< TData = unknown, @@ -19,6 +20,7 @@ export function useDataConnectQuery( FlattenedQueryResult, FirebaseError >, + _callerSdkType: CallerSdkType = CallerSdkType.TanstackReactCore ) { let queryRef: QueryRef; let initialData: FlattenedQueryResult | undefined; @@ -34,7 +36,8 @@ export function useDataConnectQuery( } else { queryRef = refOrResult; } - + // @ts-expect-error function is hidden under `DataConnect`. + queryRef.dataConnect._setCallerSdkType(_callerSdkType); return useQuery, FirebaseError>({ ...options, initialData,