From 0221d4c01cd3505ebac8afaf668bc076c2cb0467 Mon Sep 17 00:00:00 2001 From: Daniel Cortes Date: Fri, 12 Apr 2024 17:43:01 -0600 Subject: [PATCH] Have facade take a type argument instead of relying on satisfies --- packages/data-facade/README.md | 14 +++ packages/data-facade/src/facade.ts | 62 ++++++------- packages/data-facade/src/index.ts | 2 +- packages/data-facade/src/types.ts | 87 ++++++++----------- packages/mobile-app/app/(tabs)/transact.tsx | 4 +- packages/mobile-app/data/accounts/handlers.ts | 14 ++- packages/mobile-app/data/accounts/types.ts | 8 +- packages/mobile-app/data/index.ts | 8 +- 8 files changed, 106 insertions(+), 93 deletions(-) create mode 100644 packages/data-facade/README.md diff --git a/packages/data-facade/README.md b/packages/data-facade/README.md new file mode 100644 index 0000000..d81be68 --- /dev/null +++ b/packages/data-facade/README.md @@ -0,0 +1,14 @@ +# data-facade + +An abstraction layer between a React application and its underlying data source. + +## Motivation + +The initial version of the mobile app will get its data from the `wallet-server`, a server that gets data from an Iron Fish node. Though the data source is fixed for now, we want to make it easy to switch to a different data source in the future. By using this abstraction layer, we'll be able to switch between data sources without needing to make changes to the React components themselves. + +## Usage + +1. Define your interface and create handlers +2. Create facade context and export `FacadeProvider` and `useFacade` +3. Wrap your application with `FacadeProvider` +4. Use `useFacade` hook to access the facade in your components diff --git a/packages/data-facade/src/facade.ts b/packages/data-facade/src/facade.ts index 52269f9..3c31e25 100644 --- a/packages/data-facade/src/facade.ts +++ b/packages/data-facade/src/facade.ts @@ -2,42 +2,43 @@ import { ZodTypeAny, z } from "zod"; import { useQuery, useMutation } from "@tanstack/react-query"; import { buildQueryKey } from "./utils"; import type { - Expect, - Equal, ResolverFunc, - UseQueryType, - HandlerQueryBuilder, - FacadeFn, - UseMutationType, + HandlerMutationBuilderReturn, + HandlerQueryBuilderReturn, } from "./types"; -// Query handlers +// QUERY HANDLERS function buildUseQuery(baseQueryKey: string) { - return (resolver: TResolver) => ({ - useQuery: (args?: unknown) => { - return useQuery({ - queryKey: [baseQueryKey, ...buildQueryKey(args)], - queryFn: () => resolver(args), - }); - }, - }); + return (resolver: TResolver) => { + return { + useQuery: (args?: unknown) => { + return useQuery({ + queryKey: [baseQueryKey, ...buildQueryKey(args)], + queryFn: () => resolver(args), + }); + }, + }; + }; } function handlerQueryBuilder( resolver: TResolver, -): (baseQueryKey: string) => { - useQuery: UseQueryType; -} { +): HandlerQueryBuilderReturn { return (baseQueryKey: string) => buildUseQuery(baseQueryKey)(resolver); } -// Mutation handlers +// MUTATION HANDLERS function buildUseMutation() { return (resolver: TResolver) => ({ useMutation: () => { - return useMutation, Error, unknown, unknown>({ + return useMutation< + Awaited>, + Error, + unknown, + unknown + >({ mutationFn: resolver, }); }, @@ -46,13 +47,11 @@ function buildUseMutation() { function handlerMutationBuilder( resolver: TResolver, -): () => { - useMutation: UseMutationType; -} { +): HandlerMutationBuilderReturn { return () => buildUseMutation()(resolver); } -// Input util +// INPUT UTIL function handlerInputBuilder(_schema: TSchema) { return { @@ -61,16 +60,20 @@ function handlerInputBuilder(_schema: TSchema) { ) => { return handlerQueryBuilder(resolver); }, + mutation: >>( + resolver: (args: Parameters[0]) => ReturnType, + ) => { + return handlerMutationBuilder(resolver); + }, }; } -// Facade +// FACADE FUNCTION function facade< THandlers extends Record< string, - | ReturnType - | ReturnType + ReturnType >, >(handlers: THandlers) { const result: Record = {}; @@ -82,10 +85,7 @@ function facade< return result as { [K in keyof THandlers]: ReturnType }; } -type assertions = [ - Expect>, - Expect>, -]; +// EXPORTS export const f = { facade, diff --git a/packages/data-facade/src/index.ts b/packages/data-facade/src/index.ts index bf9d121..686edf8 100644 --- a/packages/data-facade/src/index.ts +++ b/packages/data-facade/src/index.ts @@ -1,3 +1,3 @@ -export type { FacadeDefinition, Query, Mutation } from "./types"; +export type { Query, Mutation } from "./types"; export { f } from "./facade"; export { createFacadeContext } from "./react-context"; diff --git a/packages/data-facade/src/types.ts b/packages/data-facade/src/types.ts index 0d586ff..98ce6dd 100644 --- a/packages/data-facade/src/types.ts +++ b/packages/data-facade/src/types.ts @@ -2,73 +2,62 @@ import { UndefinedInitialDataOptions, UseQueryResult, UseMutationResult, - UseMutateFunction, UseMutationOptions, } from "@tanstack/react-query"; -export type Expect = T; +export type ResolverFunc = (args: T) => any; -export type Equal = - (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 - ? true - : false; +// Query type utils -export type UseQueryOptions = UndefinedInitialDataOptions< - any, - Error, - any, - unknown[] ->; +type UseQueryOptions = UndefinedInitialDataOptions; -export type ResolverFunc = (opts: T) => any; +type UseQueryType< + TResolver extends ResolverFunc, + TReturn = Awaited>, +> = Parameters["length"] extends 0 + ? ( + args?: null | undefined, + opts?: UseQueryOptions | undefined, + ) => UseQueryResult + : ( + args: Parameters[0], + opts?: UseQueryOptions | undefined, + ) => UseQueryResult; -export type UseQueryType = - Parameters["length"] extends 0 - ? ( - args?: null, - opts?: UseQueryOptions, - ) => UseQueryResult> - : ( - args: Parameters[0], - opts?: UseQueryOptions, - ) => UseQueryResult>; - -export type UseMutationType = ( - opts?: UseMutationOptions, -) => UseMutationResult>; - -export type HandlerQueryBuilder = ( - func: TResolver, -) => (baseQueryKey: string) => { +export type HandlerQueryBuilderReturn = ( + baseQueryKey: string, +) => { useQuery: UseQueryType; }; -export type HandlerMutationBuilder = ( - func: TResolver, -) => () => { - useMutation: UseMutationType; -}; +// Mutation type utils + +type UseMutationType< + TResolver extends ResolverFunc, + TReturn = Awaited>, +> = (opts?: UseMutationOptions) => UseMutationResult; + +export type HandlerMutationBuilderReturn = + () => { + useMutation: UseMutationType; + }; + +// Facade function type export type FacadeFn = < THandlers extends Record< string, - ReturnType | ReturnType + | HandlerQueryBuilderReturn + | HandlerMutationBuilderReturn >, >( handlers: THandlers, ) => { [K in keyof THandlers]: ReturnType }; -export type Query = { - useQuery: UseQueryType>; -}; +// Externally consumed types -export type Mutation = { - useMutation: UseMutationType>; -}; +export type Query = + HandlerQueryBuilderReturn; -export type FacadeDefinition< - TDefinition extends Record, -> = { - // @todo: Type this correctly - [K in keyof TDefinition]: any; -}; +export type Mutation = + HandlerMutationBuilderReturn; diff --git a/packages/mobile-app/app/(tabs)/transact.tsx b/packages/mobile-app/app/(tabs)/transact.tsx index b3c565f..b4891fc 100644 --- a/packages/mobile-app/app/(tabs)/transact.tsx +++ b/packages/mobile-app/app/(tabs)/transact.tsx @@ -1,3 +1,4 @@ +import { useMutation } from "@tanstack/react-query"; import { View, Text } from "react-native"; import { useFacade } from "../../data"; import { Button } from "@ironfish/ui"; @@ -5,14 +6,13 @@ import { useState } from "react"; export default function Transact() { const [facadeResult, setFacadeResult] = useState([""]); - const facade = useFacade(); + const getAccountsResult = facade.getAccounts.useQuery(123); const getAccountsWithZodResult = facade.getAccountsWithZod.useQuery({ limit: 2, }); const getAllAccountsResult = facade.getAllAccounts.useQuery(); - const createAccount = facade.createAccount.useMutation(); return ( diff --git a/packages/mobile-app/data/accounts/handlers.ts b/packages/mobile-app/data/accounts/handlers.ts index 51c7e16..3526213 100644 --- a/packages/mobile-app/data/accounts/handlers.ts +++ b/packages/mobile-app/data/accounts/handlers.ts @@ -1,5 +1,6 @@ import { f } from "data-facade"; import { z } from "zod"; +import { AccountsMethods } from "./types"; const accounts = ["alice", "bob", "carol"]; @@ -8,7 +9,7 @@ async function getAccounts(limit: number) { return accounts.slice(0, limit); } -export const accountsHandlers = f.facade({ +export const accountsHandlers = f.facade({ getAccounts: f.handler.query(async (count: number) => { const accounts = await getAccounts(count ?? 1); console.log("getAccounts", accounts); @@ -35,4 +36,15 @@ export const accountsHandlers = f.facade({ accounts.push(account); return accounts; }), + createAccountWithZod: f.handler + .input( + z.object({ + account: z.string(), + }), + ) + .mutation(async ({ account }) => { + console.log("createAccountWithZod", account); + accounts.push(account); + return accounts; + }), }); diff --git a/packages/mobile-app/data/accounts/types.ts b/packages/mobile-app/data/accounts/types.ts index e9bbd67..f294078 100644 --- a/packages/mobile-app/data/accounts/types.ts +++ b/packages/mobile-app/data/accounts/types.ts @@ -1,7 +1,9 @@ -import { FacadeDefinition, Query } from "data-facade"; +import { Query, Mutation } from "data-facade"; -export type AccountsMethods = FacadeDefinition<{ +export type AccountsMethods = { getAccounts: Query<(count: number) => string[]>; getAllAccounts: Query<() => string[]>; getAccountsWithZod: Query<(args: { limit: number }) => string[]>; -}>; + createAccount: Mutation<(account: string) => string[]>; + createAccountWithZod: Mutation<(args: { account: string }) => string[]>; +}; diff --git a/packages/mobile-app/data/index.ts b/packages/mobile-app/data/index.ts index eb15732..34d0689 100644 --- a/packages/mobile-app/data/index.ts +++ b/packages/mobile-app/data/index.ts @@ -1,11 +1,7 @@ -import { accountsHandlers } from "./accounts/handlers"; -import { AccountsMethods } from "./accounts/types"; - import { createFacadeContext } from "data-facade"; +import { accountsHandlers } from "./accounts/handlers"; -const facadeContext = createFacadeContext( - accountsHandlers satisfies AccountsMethods, -); +const facadeContext = createFacadeContext(accountsHandlers); export const FacadeProvider = facadeContext.Provider; export const useFacade = facadeContext.useFacade;