Skip to content

Commit

Permalink
Have facade take a type argument instead of relying on satisfies
Browse files Browse the repository at this point in the history
  • Loading branch information
dgca committed Apr 12, 2024
1 parent 95fd47b commit 0221d4c
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 93 deletions.
14 changes: 14 additions & 0 deletions packages/data-facade/README.md
Original file line number Diff line number Diff line change
@@ -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
62 changes: 31 additions & 31 deletions packages/data-facade/src/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <TResolver extends ResolverFunc>(resolver: TResolver) => ({
useQuery: (args?: unknown) => {
return useQuery({
queryKey: [baseQueryKey, ...buildQueryKey(args)],
queryFn: () => resolver(args),
});
},
});
return <TResolver extends ResolverFunc>(resolver: TResolver) => {
return {
useQuery: (args?: unknown) => {
return useQuery({
queryKey: [baseQueryKey, ...buildQueryKey(args)],
queryFn: () => resolver(args),
});
},
};
};
}

function handlerQueryBuilder<TResolver extends ResolverFunc>(
resolver: TResolver,
): (baseQueryKey: string) => {
useQuery: UseQueryType<TResolver>;
} {
): HandlerQueryBuilderReturn<TResolver> {
return (baseQueryKey: string) => buildUseQuery(baseQueryKey)(resolver);
}

// Mutation handlers
// MUTATION HANDLERS

function buildUseMutation() {
return <TResolver extends ResolverFunc>(resolver: TResolver) => ({
useMutation: () => {
return useMutation<ReturnType<TResolver>, Error, unknown, unknown>({
return useMutation<
Awaited<ReturnType<TResolver>>,
Error,
unknown,
unknown
>({
mutationFn: resolver,
});
},
Expand All @@ -46,13 +47,11 @@ function buildUseMutation() {

function handlerMutationBuilder<TResolver extends ResolverFunc>(
resolver: TResolver,
): () => {
useMutation: UseMutationType<TResolver>;
} {
): HandlerMutationBuilderReturn<TResolver> {
return () => buildUseMutation()(resolver);
}

// Input util
// INPUT UTIL

function handlerInputBuilder<TSchema extends ZodTypeAny>(_schema: TSchema) {
return {
Expand All @@ -61,16 +60,20 @@ function handlerInputBuilder<TSchema extends ZodTypeAny>(_schema: TSchema) {
) => {
return handlerQueryBuilder(resolver);
},
mutation: <TResolver extends ResolverFunc<z.infer<TSchema>>>(
resolver: (args: Parameters<TResolver>[0]) => ReturnType<TResolver>,
) => {
return handlerMutationBuilder(resolver);
},
};
}

// Facade
// FACADE FUNCTION

function facade<
THandlers extends Record<
string,
| ReturnType<typeof handlerQueryBuilder>
| ReturnType<typeof handlerMutationBuilder>
ReturnType<typeof handlerQueryBuilder | typeof handlerMutationBuilder>
>,
>(handlers: THandlers) {
const result: Record<string, any> = {};
Expand All @@ -82,10 +85,7 @@ function facade<
return result as { [K in keyof THandlers]: ReturnType<THandlers[K]> };
}

type assertions = [
Expect<Equal<typeof handlerQueryBuilder, HandlerQueryBuilder>>,
Expect<Equal<typeof facade, FacadeFn>>,
];
// EXPORTS

export const f = {
facade,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-facade/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
87 changes: 38 additions & 49 deletions packages/data-facade/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,62 @@ import {
UndefinedInitialDataOptions,
UseQueryResult,
UseMutationResult,
UseMutateFunction,
UseMutationOptions,
} from "@tanstack/react-query";

export type Expect<T extends true> = T;
export type ResolverFunc<T = any> = (args: T) => any;

export type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? true
: false;
// Query type utils

export type UseQueryOptions = UndefinedInitialDataOptions<
any,
Error,
any,
unknown[]
>;
type UseQueryOptions = UndefinedInitialDataOptions<any, Error, any, unknown[]>;

export type ResolverFunc<T = any> = (opts: T) => any;
type UseQueryType<
TResolver extends ResolverFunc,
TReturn = Awaited<ReturnType<TResolver>>,
> = Parameters<TResolver>["length"] extends 0
? (
args?: null | undefined,
opts?: UseQueryOptions | undefined,
) => UseQueryResult<TReturn>
: (
args: Parameters<TResolver>[0],
opts?: UseQueryOptions | undefined,
) => UseQueryResult<TReturn>;

export type UseQueryType<TResolver extends ResolverFunc> =
Parameters<TResolver>["length"] extends 0
? (
args?: null,
opts?: UseQueryOptions,
) => UseQueryResult<ReturnType<TResolver>>
: (
args: Parameters<TResolver>[0],
opts?: UseQueryOptions,
) => UseQueryResult<ReturnType<TResolver>>;

export type UseMutationType<TResolver extends ResolverFunc> = (
opts?: UseMutationOptions,
) => UseMutationResult<ReturnType<TResolver>>;

export type HandlerQueryBuilder = <TResolver extends ResolverFunc>(
func: TResolver,
) => (baseQueryKey: string) => {
export type HandlerQueryBuilderReturn<TResolver extends ResolverFunc> = (
baseQueryKey: string,
) => {
useQuery: UseQueryType<TResolver>;
};

export type HandlerMutationBuilder = <TResolver extends ResolverFunc>(
func: TResolver,
) => () => {
useMutation: UseMutationType<TResolver>;
};
// Mutation type utils

type UseMutationType<
TResolver extends ResolverFunc,
TReturn = Awaited<ReturnType<TResolver>>,
> = (opts?: UseMutationOptions) => UseMutationResult<TReturn>;

export type HandlerMutationBuilderReturn<TResolver extends ResolverFunc> =
() => {
useMutation: UseMutationType<TResolver>;
};

// Facade function type

export type FacadeFn = <
THandlers extends Record<
string,
ReturnType<HandlerQueryBuilder> | ReturnType<HandlerMutationBuilder>
| HandlerQueryBuilderReturn<ResolverFunc>
| HandlerMutationBuilderReturn<ResolverFunc>
>,
>(
handlers: THandlers,
) => { [K in keyof THandlers]: ReturnType<THandlers[K]> };

export type Query<T extends any = any> = {
useQuery: UseQueryType<ResolverFunc<T>>;
};
// Externally consumed types

export type Mutation<T extends any = any> = {
useMutation: UseMutationType<ResolverFunc<T>>;
};
export type Query<TResolver extends ResolverFunc> =
HandlerQueryBuilderReturn<TResolver>;

export type FacadeDefinition<
TDefinition extends Record<string, Query | Mutation>,
> = {
// @todo: Type this correctly
[K in keyof TDefinition]: any;
};
export type Mutation<TResolver extends ResolverFunc> =
HandlerMutationBuilderReturn<TResolver>;
4 changes: 2 additions & 2 deletions packages/mobile-app/app/(tabs)/transact.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { useMutation } from "@tanstack/react-query";
import { View, Text } from "react-native";
import { useFacade } from "../../data";
import { Button } from "@ironfish/ui";
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 (
Expand Down
14 changes: 13 additions & 1 deletion packages/mobile-app/data/accounts/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { f } from "data-facade";
import { z } from "zod";
import { AccountsMethods } from "./types";

const accounts = ["alice", "bob", "carol"];

Expand All @@ -8,7 +9,7 @@ async function getAccounts(limit: number) {
return accounts.slice(0, limit);
}

export const accountsHandlers = f.facade({
export const accountsHandlers = f.facade<AccountsMethods>({
getAccounts: f.handler.query(async (count: number) => {
const accounts = await getAccounts(count ?? 1);
console.log("getAccounts", accounts);
Expand All @@ -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;
}),
});
8 changes: 5 additions & 3 deletions packages/mobile-app/data/accounts/types.ts
Original file line number Diff line number Diff line change
@@ -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[]>;
};
8 changes: 2 additions & 6 deletions packages/mobile-app/data/index.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 0221d4c

Please sign in to comment.