react-fate 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -62
- package/lib/index.d.mts +8 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,21 +26,42 @@
|
|
|
26
26
|
|
|
27
27
|
GraphQL and Relay introduced several novel ideas: fragments co‑located with components, [a normalized cache](https://relay.dev/docs/principles-and-architecture/thinking-in-graphql/#caching-a-graph) keyed by global identifiers, and a compiler that hoists fragments into a single network request. These innovations made it possible to build large applications where data requirements are modular and self‑contained.
|
|
28
28
|
|
|
29
|
-
Nakazawa Tech builds apps primarily with GraphQL and Relay. We advocate for these technologies in [talks](https://www.youtube.com/watch?v=rxPTEko8J7c&t=36s) and provide templates ([server](https://github.com/nkzw-tech/server-template), [client](https://github.com/nkzw-tech/web-app-template/tree/with-relay)) to help developers get started quickly.
|
|
29
|
+
[Nakazawa Tech](https://nakazawa.tech) builds apps and [games](https://athenacrisis.com) primarily with GraphQL and Relay. We advocate for these technologies in [talks](https://www.youtube.com/watch?v=rxPTEko8J7c&t=36s) and provide templates ([server](https://github.com/nkzw-tech/server-template), [client](https://github.com/nkzw-tech/web-app-template/tree/with-relay)) to help developers get started quickly.
|
|
30
30
|
|
|
31
31
|
However, GraphQL comes with its own type system and query language. If you are already using tRPC or another type‑safe RPC framework, it's a significant investment to adopt and implement GraphQL on the backend. This investment often prevents teams from adopting Relay on the frontend.
|
|
32
32
|
|
|
33
33
|
Many React data frameworks lack Relay's ergonomics, especially fragment composition, co-located data requirements, predictable caching, and deep integration with modern React features. Optimistic updates usually require manually managing keys and imperative data updates, which is error-prone and tedious.
|
|
34
34
|
|
|
35
|
-
fate takes the great ideas from Relay and puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching.
|
|
35
|
+
fate takes the great ideas from Relay and puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching. Using _fate_ usually looks like this:
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
```tsx
|
|
38
|
+
export const PostView = view<Post>()({
|
|
39
|
+
content: true,
|
|
40
|
+
id: true,
|
|
41
|
+
title: true,
|
|
42
|
+
author: UserView,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
46
|
+
const post = useView(PostView, postRef);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Card>
|
|
50
|
+
<h2>{post.title}</h2>
|
|
51
|
+
<p>{post.content}</p>
|
|
52
|
+
<UserCard user={post.author} />
|
|
53
|
+
</Card>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
_[Learn more](/docs/guide/getting-started.md) about fate's core concepts or get started with [a ready-made template](https://github.com/nkzw-tech/fate-template#readme)._
|
|
38
59
|
|
|
39
60
|
## Getting Started
|
|
40
61
|
|
|
41
62
|
### Template
|
|
42
63
|
|
|
43
|
-
Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template) quickly:
|
|
64
|
+
Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template#readme) quickly:
|
|
44
65
|
|
|
45
66
|
::: code-group
|
|
46
67
|
|
|
@@ -58,7 +79,7 @@ yarn dlx giget@latest gh:nkzw-tech/fate-template
|
|
|
58
79
|
|
|
59
80
|
:::
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
`fate-template` comes with a simple tRPC backend and a React frontend using **_fate_**. It features modern tools to deliver an incredibly fast development experience. Follow its [README.md](https://github.com/nkzw-tech/fate-template#fate-quick-start-template) to get started.
|
|
62
83
|
|
|
63
84
|
### Manual Installation
|
|
64
85
|
|
|
@@ -126,6 +147,9 @@ Traditionally, React apps are built with components and hooks. fate introduces a
|
|
|
126
147
|
|
|
127
148
|
With fate, you no longer worry about _when_ to fetch data, how to coordinate loading states, or how to handle errors imperatively. You avoid overfetching, stop passing unnecessary data down the tree, and eliminate boilerplate types created solely for passing server data to child components.
|
|
128
149
|
|
|
150
|
+
> [!NOTE]
|
|
151
|
+
> Views in _fate_ are what fragments are in GraphQL.
|
|
152
|
+
|
|
129
153
|
## Views
|
|
130
154
|
|
|
131
155
|
### Defining Views
|
|
@@ -525,7 +549,11 @@ Mutations in your tRPC backend are made available as actions and mutations by fa
|
|
|
525
549
|
Let's assume that our `Post` entity has a tRPC mutation for liking a post called `post.like`. A `LikeButton` component using fate Actions and an async component library could then look like this:
|
|
526
550
|
|
|
527
551
|
```tsx
|
|
552
|
+
import { useActionState } from 'react';
|
|
553
|
+
import { useFateClient } from 'react-fate';
|
|
554
|
+
|
|
528
555
|
const LikeButton = ({ post }: { post: { id: string; likes: number } }) => {
|
|
556
|
+
const fate = useFateClient();
|
|
529
557
|
const [result, like] = useActionState(fate.actions.post.like, null);
|
|
530
558
|
|
|
531
559
|
return (
|
|
@@ -540,6 +568,7 @@ If you are not using an async component library, you can use React's `useTransit
|
|
|
540
568
|
|
|
541
569
|
```tsx
|
|
542
570
|
const LikeButton = ({ post }: { post: { id: string; likes: number } }) => {
|
|
571
|
+
const fate = useFateClient();
|
|
543
572
|
const [, startTransition] = useTransition();
|
|
544
573
|
const [result, like, isPending] = useActionState(
|
|
545
574
|
fate.actions.post.like,
|
|
@@ -661,16 +690,17 @@ export const postRouter = router({
|
|
|
661
690
|
view: postDataView,
|
|
662
691
|
});
|
|
663
692
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
693
|
+
return resolve(
|
|
694
|
+
await ctx.prisma.post.update({
|
|
695
|
+
data: {
|
|
696
|
+
likes: {
|
|
697
|
+
increment: 1,
|
|
698
|
+
},
|
|
668
699
|
},
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
return resolve(updated as unknown as PostItem);
|
|
700
|
+
select,
|
|
701
|
+
where: { id: input.id },
|
|
702
|
+
} as PostUpdateArgs),
|
|
703
|
+
);
|
|
674
704
|
}),
|
|
675
705
|
});
|
|
676
706
|
```
|
|
@@ -739,6 +769,26 @@ useEffect(() => {
|
|
|
739
769
|
}, [like, result]);
|
|
740
770
|
```
|
|
741
771
|
|
|
772
|
+
### Controlling List Insertion Behavior
|
|
773
|
+
|
|
774
|
+
When inserting new objects into lists, the default behavior is to append the new object to the list. You can provide an `insert` option with `before`, `after` or `none` values to customize this behavior and specify where the new object should be inserted in the list:
|
|
775
|
+
|
|
776
|
+
```tsx
|
|
777
|
+
addComment({
|
|
778
|
+
input: { content: 'New Comment text', postId: post.id },
|
|
779
|
+
insert: 'before', // Insert the new comment at the beginning of the list.
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
Or, use the `none` option if you want to ignore inserting the new object into any lists:
|
|
784
|
+
|
|
785
|
+
```tsx
|
|
786
|
+
addComment({
|
|
787
|
+
input: { content: 'New Comment text', postId: post.id },
|
|
788
|
+
insert: 'none', // Do not insert the new comment into any lists.
|
|
789
|
+
});
|
|
790
|
+
```
|
|
791
|
+
|
|
742
792
|
## Server Integration
|
|
743
793
|
|
|
744
794
|
Until now, we have focused on the client-side API of fate. You'll need a tRPC backend that follows some conventions so you can generate a typed client using fate's CLI. At the moment _fate_ is designed to work with tRPC and Prisma, but the framework is not coupled to any particular ORM or database, it's just what we are starting with.
|
|
@@ -785,35 +835,27 @@ _Note: Currently, fate provides helpers to integrate with Prisma, but the framew
|
|
|
785
835
|
We can apply the above data view in our tRPC router and resolve the client's selection against it using `createResolver`. Here is an example implementation of the `byId` query for the `User` type which allows fetching multiple users by `id`:
|
|
786
836
|
|
|
787
837
|
```tsx
|
|
788
|
-
import {
|
|
838
|
+
import { byIdInput, createResolver } from '@nkzw/fate/server';
|
|
789
839
|
import { z } from 'zod';
|
|
790
840
|
import type { UserFindManyArgs } from '../../prisma/prisma-client/models.ts';
|
|
791
841
|
import { procedure, router } from '../init.ts';
|
|
792
842
|
import { userDataView } from '../views.ts';
|
|
793
843
|
|
|
794
844
|
export const userRouter = router({
|
|
795
|
-
byId: procedure
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const users = await ctx.prisma.user.findMany({
|
|
811
|
-
select: select,
|
|
812
|
-
where: { id: { in: input.ids } },
|
|
813
|
-
} as UserFindManyArgs);
|
|
814
|
-
|
|
815
|
-
return await resolveMany(users);
|
|
816
|
-
}),
|
|
845
|
+
byId: procedure.input(byIdInput).query(async ({ ctx, input }) => {
|
|
846
|
+
const { resolveMany, select } = createResolver({
|
|
847
|
+
...input,
|
|
848
|
+
ctx,
|
|
849
|
+
view: userDataView,
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
const users = await ctx.prisma.user.findMany({
|
|
853
|
+
select: select,
|
|
854
|
+
where: { id: { in: input.ids } },
|
|
855
|
+
} as UserFindManyArgs);
|
|
856
|
+
|
|
857
|
+
return await resolveMany(users);
|
|
858
|
+
}),
|
|
817
859
|
});
|
|
818
860
|
```
|
|
819
861
|
|
|
@@ -894,29 +936,19 @@ export const postDataView = dataView<PostItem>('Post')({
|
|
|
894
936
|
});
|
|
895
937
|
```
|
|
896
938
|
|
|
897
|
-
We can
|
|
939
|
+
We can define extra root-level lists and queries by exporting a `Root` object from our `views.ts` file using the same view syntax as everywhere else:
|
|
898
940
|
|
|
899
941
|
```tsx
|
|
900
|
-
export const
|
|
901
|
-
|
|
942
|
+
export const Root = {
|
|
943
|
+
categories: list(categoryDataView),
|
|
944
|
+
commentSearch: { procedure: 'search', view: list(commentDataView) },
|
|
945
|
+
events: list(eventDataView),
|
|
946
|
+
posts: list(postDataView),
|
|
947
|
+
viewer: userDataView,
|
|
902
948
|
};
|
|
903
949
|
```
|
|
904
950
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
#### Custom Root Lists
|
|
908
|
-
|
|
909
|
-
You might want to define custom root lists that don't directly map to a single data view. For example, a search endpoint that returns a list of posts based on a search query:
|
|
910
|
-
|
|
911
|
-
```tsx
|
|
912
|
-
export const Lists = {
|
|
913
|
-
// …
|
|
914
|
-
postSearch: { procedure: 'search', view: postDataView },
|
|
915
|
-
// …
|
|
916
|
-
};
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
This maps the `postSearch` list to a `search` procedure on your post router.
|
|
951
|
+
Entries that wrap their view in `list(...)` are treated as list resolvers and use the `procedure` name when calling the corresponding router procedure, defaulting to `list`. If you omit `list(...)`, fate treats the entry as a standard query and uses the view type name to infer the router name.
|
|
920
952
|
|
|
921
953
|
### Data View Resolvers
|
|
922
954
|
|
|
@@ -925,19 +957,33 @@ fate data views support resolvers for computed fields. If we want to add a `comm
|
|
|
925
957
|
```tsx
|
|
926
958
|
export const postDataView = dataView<PostItem>('Post')({
|
|
927
959
|
author: userDataView,
|
|
928
|
-
commentCount: resolver<PostItem>({
|
|
929
|
-
resolve: ({
|
|
960
|
+
commentCount: resolver<PostItem, number>({
|
|
961
|
+
resolve: ({ _count }) => _count?.comments ?? 0,
|
|
930
962
|
select: () => ({
|
|
931
963
|
_count: { select: { comments: true } },
|
|
932
964
|
}),
|
|
933
965
|
}),
|
|
934
966
|
comments: list(commentDataView),
|
|
935
967
|
id: true,
|
|
936
|
-
}
|
|
968
|
+
});
|
|
937
969
|
```
|
|
938
970
|
|
|
939
971
|
This definition makes the `commentCount` field available to your client-side views.
|
|
940
972
|
|
|
973
|
+
### Authorization in Resolvers
|
|
974
|
+
|
|
975
|
+
You might want to restrict access to certain fields based on the current user or other contextual information. You can do this by adding an `authorize` function to your resolver definition:
|
|
976
|
+
|
|
977
|
+
```tsx
|
|
978
|
+
export const userDataView = dataView<UserItem>('User')({
|
|
979
|
+
email: resolver<UserItem, string | null, { sessionUser: string }>({
|
|
980
|
+
authorize: ({ id }, context) => context?.sessionUserId === id,
|
|
981
|
+
resolve: ({ email }) => email,
|
|
982
|
+
}),
|
|
983
|
+
id: true,
|
|
984
|
+
});
|
|
985
|
+
```
|
|
986
|
+
|
|
941
987
|
### Generating a typed client
|
|
942
988
|
|
|
943
989
|
Now that we have defined our client views and our tRPC server, we need to connect them with some glue code. We recommend using fate's CLI for convenience.
|
|
@@ -959,10 +1005,10 @@ export type AppRouter = typeof appRouter;
|
|
|
959
1005
|
export * from './views.ts';
|
|
960
1006
|
```
|
|
961
1007
|
|
|
962
|
-
_Note: We try to keep magic to a minimum and you can handwrite the [generated client](https://github.com/nkzw-tech/fate/blob/main/example/client/src/
|
|
1008
|
+
_Note: We try to keep magic to a minimum and you can handwrite the [generated client](https://github.com/nkzw-tech/fate/blob/main/example/client/src/fate.ts) if you prefer._
|
|
963
1009
|
|
|
964
1010
|
```bash
|
|
965
|
-
pnpm fate generate @your-org/server/trpc/router.ts client/src/
|
|
1011
|
+
pnpm fate generate @your-org/server/trpc/router.ts client/src/fate.ts
|
|
966
1012
|
```
|
|
967
1013
|
|
|
968
1014
|
_Note: fate uses the specified server module name to extract the server types it needs and uses the same module name to import the views into the generated client. Make sure that the module is available both at the root where you are running the CLI and in the client package._
|
|
@@ -1024,7 +1070,9 @@ Probably. One day. _Maybe._
|
|
|
1024
1070
|
### How was fate built?
|
|
1025
1071
|
|
|
1026
1072
|
> [!NOTE]
|
|
1027
|
-
> 80% of _fate_'s code was written by OpenAI's Codex – four versions per task, carefully curated by a human. The remaining 20% was written by [@cnakazawa](https://x.com/cnakazawa).
|
|
1073
|
+
> 80% of _fate_'s code was written by OpenAI's Codex – four versions per task, carefully curated by a human. The remaining 20% was written by [@cnakazawa](https://x.com/cnakazawa). _You get to decide which parts are the good ones!_ The docs were 100% written by a human.
|
|
1074
|
+
>
|
|
1075
|
+
> If you contribute to _fate_, we [require you to disclose your use of AI tools](https://github.com/nkzw-tech/fate/blob/main/CONTRIBUTING.md#ai-assistance-notice).
|
|
1028
1076
|
|
|
1029
1077
|
## Future
|
|
1030
1078
|
|
|
@@ -1041,5 +1089,6 @@ Probably. One day. _Maybe._
|
|
|
1041
1089
|
|
|
1042
1090
|
- [Relay](https://relay.dev/), [Isograph](https://isograph.dev/) & [GraphQL](https://graphql.org/) for inspiration
|
|
1043
1091
|
- [Ricky Hanlon](https://x.com/rickyfm) for guidance on Async React
|
|
1092
|
+
- [Anthony Powell](https://x.com/Cephalization) for testing fate and providing feedback
|
|
1044
1093
|
|
|
1045
|
-
**_fate_** was created by [@cnakazawa](https://x.com/cnakazawa) and is maintained by [Nakazawa Tech](https://nakazawa.tech/).
|
|
1094
|
+
**_fate_** was created by [@cnakazawa](https://x.com/cnakazawa) and is maintained by [Nakazawa Tech](https://nakazawa.tech/).
|
package/lib/index.d.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { ConnectionRef, FateClient as FateClient$1, Pagination, Request, RequestOptions, RequestResult, View, ViewData, ViewEntity, ViewEntityName, ViewRef, ViewRef as ViewRef$1, ViewSelection, createClient, createTRPCTransport, mutation, view } from "@nkzw/fate";
|
|
1
|
+
import { ConnectionRef, FateClient as FateClient$1, FateMutations, Pagination, Request, RequestOptions, RequestResult, View, ViewData, ViewEntity, ViewEntityName, ViewRef, ViewRef as ViewRef$1, ViewSelection, createClient, createTRPCTransport, mutation, view } from "@nkzw/fate";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/context.d.ts
|
|
6
|
+
type Mutations = keyof ClientMutations extends never ? FateMutations : ClientMutations;
|
|
6
7
|
/**
|
|
7
8
|
* Provider component that supplies a configured `FateClient` to React hooks.
|
|
8
9
|
*/
|
|
@@ -11,12 +12,12 @@ declare function FateClient({
|
|
|
11
12
|
client
|
|
12
13
|
}: {
|
|
13
14
|
children: ReactNode;
|
|
14
|
-
client: FateClient$1
|
|
15
|
+
client: FateClient$1<any>;
|
|
15
16
|
}): react_jsx_runtime0.JSX.Element;
|
|
16
17
|
/**
|
|
17
18
|
* Returns the nearest `FateClient` from context.
|
|
18
19
|
*/
|
|
19
|
-
declare function useFateClient(): FateClient$1
|
|
20
|
+
declare function useFateClient<M extends Mutations>(): FateClient$1<M>;
|
|
20
21
|
//#endregion
|
|
21
22
|
//#region src/useView.d.ts
|
|
22
23
|
type ViewEntityWithTypename<V extends View<any, any>> = ViewEntity<V> & {
|
|
@@ -59,4 +60,7 @@ declare function useListView<C extends {
|
|
|
59
60
|
pagination?: Pagination;
|
|
60
61
|
} | null | undefined>(selection: ConnectionSelection, connection: C): [ConnectionItems<NonNullable<C>>, LoadMoreFn | null, LoadMoreFn | null];
|
|
61
62
|
//#endregion
|
|
62
|
-
|
|
63
|
+
//#region src/index.d.ts
|
|
64
|
+
interface ClientMutations {}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { ClientMutations, type ConnectionRef, FateClient, type ViewRef, createClient, createTRPCTransport, mutation, useFateClient, useListView, useRequest, useView, view };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-fate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "fate is a modern data client for React.",
|
|
5
5
|
"homepage": "https://github.com/nkzw-tech/fate",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lib"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@nkzw/fate": "^0.0.
|
|
31
|
+
"@nkzw/fate": "^0.0.8"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/react": "^19.2.7",
|