@nkzw/fate 0.1.2 → 1.0.0-rc.0
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 +810 -129
- package/docs/api/classes/FateClient.md +832 -0
- package/docs/api/functions/clientRoot.md +27 -0
- package/docs/api/functions/createClient.md +21 -0
- package/docs/api/functions/createHTTPTransport.md +51 -0
- package/docs/api/functions/createTRPCTransport.md +46 -0
- package/docs/api/functions/getListEntries.md +15 -0
- package/docs/api/functions/getSelectionPlan.md +36 -0
- package/docs/api/functions/isRecord.md +15 -0
- package/docs/api/functions/isViewTag.md +17 -0
- package/docs/api/functions/liveConnectionTopic.md +19 -0
- package/docs/api/functions/liveEntityTopic.md +19 -0
- package/docs/api/functions/liveGlobalConnectionTopic.md +15 -0
- package/docs/api/functions/mutation.md +32 -0
- package/docs/api/functions/toEntityId.md +21 -0
- package/docs/api/functions/view.md +26 -0
- package/docs/api/index.md +81 -0
- package/docs/api/interfaces/FateThenable.md +31 -0
- package/docs/api/interfaces/Transport.md +199 -0
- package/docs/api/server/classes/FateRequestError.md +67 -0
- package/docs/api/server/drizzle/functions/createDrizzleFate.md +431 -0
- package/docs/api/server/drizzle/functions/createDrizzleSourceAdapter.md +21 -0
- package/docs/api/server/drizzle/functions/createDrizzleSourceRegistry.md +21 -0
- package/docs/api/server/drizzle/index.md +23 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyConfig.md +15 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyInput.md +5 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleQueryExtra.md +21 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleSourceAdapter.md +337 -0
- package/docs/api/server/drizzle/type-aliases/DrizzleViewConfig.md +29 -0
- package/docs/api/server/functions/bindSourceProcedures.md +32 -0
- package/docs/api/server/functions/computed.md +43 -0
- package/docs/api/server/functions/count.md +21 -0
- package/docs/api/server/functions/createFateFetchHandler.md +21 -0
- package/docs/api/server/functions/createFateServer.md +41 -0
- package/docs/api/server/functions/createHonoFateHandler.md +21 -0
- package/docs/api/server/functions/createLiveEventBus.md +17 -0
- package/docs/api/server/functions/createNestedSourcePlan.md +103 -0
- package/docs/api/server/functions/createResolver.md +74 -0
- package/docs/api/server/functions/createSourcePlan.md +95 -0
- package/docs/api/server/functions/createSourceProcedures.md +46 -0
- package/docs/api/server/functions/dataView.md +32 -0
- package/docs/api/server/functions/field.md +15 -0
- package/docs/api/server/functions/getNestedSelection.md +19 -0
- package/docs/api/server/functions/getScopedArgs.md +21 -0
- package/docs/api/server/functions/hasNestedSelection.md +19 -0
- package/docs/api/server/functions/list.md +28 -0
- package/docs/api/server/functions/refetchSourceById.md +55 -0
- package/docs/api/server/functions/resolveSourceById.md +55 -0
- package/docs/api/server/functions/resolveSourceByIds.md +55 -0
- package/docs/api/server/functions/resolveSourceConnection.md +67 -0
- package/docs/api/server/functions/resolver.md +42 -0
- package/docs/api/server/functions/toPrismaSelect.md +25 -0
- package/docs/api/server/functions/withConnection.md +24 -0
- package/docs/api/server/index.md +109 -0
- package/docs/api/server/prisma/functions/createPrismaFate.md +435 -0
- package/docs/api/server/prisma/functions/createPrismaSourceAdapter.md +21 -0
- package/docs/api/server/prisma/functions/createPrismaSourceRegistry.md +21 -0
- package/docs/api/server/prisma/functions/prismaConnectionArgs.md +57 -0
- package/docs/api/server/prisma/index.md +23 -0
- package/docs/api/server/prisma/type-aliases/PrismaDelegate.md +59 -0
- package/docs/api/server/prisma/type-aliases/PrismaQueryExtra.md +5 -0
- package/docs/api/server/prisma/type-aliases/PrismaSourceAdapter.md +343 -0
- package/docs/api/server/prisma/type-aliases/PrismaViewConfig.md +31 -0
- package/docs/api/server/type-aliases/ComputedField.md +55 -0
- package/docs/api/server/type-aliases/ComputedSelection.md +5 -0
- package/docs/api/server/type-aliases/ConnectionItem.md +29 -0
- package/docs/api/server/type-aliases/ConnectionPagination.md +39 -0
- package/docs/api/server/type-aliases/ConnectionResult.md +29 -0
- package/docs/api/server/type-aliases/CountSelection.md +29 -0
- package/docs/api/server/type-aliases/CountWhere.md +5 -0
- package/docs/api/server/type-aliases/DataViewListOptions.md +13 -0
- package/docs/api/server/type-aliases/DataViewOrderBy.md +5 -0
- package/docs/api/server/type-aliases/DataViewOrderDirection.md +5 -0
- package/docs/api/server/type-aliases/DataViewResult.md +11 -0
- package/docs/api/server/type-aliases/Entity.md +21 -0
- package/docs/api/server/type-aliases/FateServer.md +75 -0
- package/docs/api/server/type-aliases/FateServerManifest.md +5 -0
- package/docs/api/server/type-aliases/FieldSelection.md +21 -0
- package/docs/api/server/type-aliases/LiveConnectionEventType.md +5 -0
- package/docs/api/server/type-aliases/LiveConnectionSourceEvent.md +5 -0
- package/docs/api/server/type-aliases/LiveEventBus.md +5 -0
- package/docs/api/server/type-aliases/LiveEventType.md +5 -0
- package/docs/api/server/type-aliases/LiveSourceEvent.md +5 -0
- package/docs/api/server/type-aliases/NativeFateAPI.md +47 -0
- package/docs/api/server/type-aliases/OrderDirection.md +5 -0
- package/docs/api/server/type-aliases/SourceConfig.md +55 -0
- package/docs/api/server/type-aliases/SourceDefinition.md +55 -0
- package/docs/api/server/type-aliases/SourceOrder.md +5 -0
- package/docs/api/server/type-aliases/SourceOrderField.md +21 -0
- package/docs/api/server/type-aliases/SourcePlan.md +29 -0
- package/docs/api/server/type-aliases/SourcePlanNode.md +29 -0
- package/docs/api/server/type-aliases/SourceRegistry.md +11 -0
- package/docs/api/server/type-aliases/SourceRelation.md +15 -0
- package/docs/api/server/type-aliases/SourceRelationConfig.md +23 -0
- package/docs/api/server/variables/byIdInput.md +5 -0
- package/docs/api/server/variables/connectionArgs.md +7 -0
- package/docs/api/type-aliases/ConnectionMetadata.md +7 -0
- package/docs/api/type-aliases/ConnectionRef.md +13 -0
- package/docs/api/type-aliases/Entity.md +15 -0
- package/docs/api/type-aliases/EntityId.md +7 -0
- package/docs/api/type-aliases/FateLiveConnectionEvent.md +5 -0
- package/docs/api/type-aliases/FateLiveEvent.md +5 -0
- package/docs/api/type-aliases/FateMutations.md +7 -0
- package/docs/api/type-aliases/FateOperation.md +5 -0
- package/docs/api/type-aliases/FateProtocolRequest.md +5 -0
- package/docs/api/type-aliases/FateProtocolResponse.md +5 -0
- package/docs/api/type-aliases/FateRecord.md +7 -0
- package/docs/api/type-aliases/FateRoots.md +7 -0
- package/docs/api/type-aliases/InferFateAPI.md +11 -0
- package/docs/api/type-aliases/List.md +5 -0
- package/docs/api/type-aliases/ListEntry.md +5 -0
- package/docs/api/type-aliases/ListItem.md +13 -0
- package/docs/api/type-aliases/Mask.md +15 -0
- package/docs/api/type-aliases/MutationDefinition.md +21 -0
- package/docs/api/type-aliases/MutationEntity.md +13 -0
- package/docs/api/type-aliases/MutationIdentifier.md +19 -0
- package/docs/api/type-aliases/MutationInput.md +13 -0
- package/docs/api/type-aliases/MutationResult.md +13 -0
- package/docs/api/type-aliases/NodesItem.md +13 -0
- package/docs/api/type-aliases/Pagination.md +39 -0
- package/docs/api/type-aliases/Request.md +7 -0
- package/docs/api/type-aliases/RequestMode.md +7 -0
- package/docs/api/type-aliases/RequestOptions.md +7 -0
- package/docs/api/type-aliases/RequestResult.md +18 -0
- package/docs/api/type-aliases/Selection.md +13 -0
- package/docs/api/type-aliases/Snapshot.md +7 -0
- package/docs/api/type-aliases/TypeConfig.md +41 -0
- package/docs/api/type-aliases/View.md +17 -0
- package/docs/api/type-aliases/ViewData.md +18 -0
- package/docs/api/type-aliases/ViewEntity.md +13 -0
- package/docs/api/type-aliases/ViewEntityName.md +13 -0
- package/docs/api/type-aliases/ViewRef.md +13 -0
- package/docs/api/type-aliases/ViewSelection.md +13 -0
- package/docs/api/type-aliases/ViewSnapshot.md +18 -0
- package/docs/api/type-aliases/ViewTag.md +7 -0
- package/docs/api/variables/ConnectionTag.md +7 -0
- package/docs/guide/actions.md +263 -0
- package/docs/guide/core-concepts.md +22 -0
- package/docs/guide/getting-started.md +57 -0
- package/docs/guide/list-views.md +86 -0
- package/docs/guide/live-views.md +245 -0
- package/docs/guide/requests.md +130 -0
- package/docs/guide/server-integration.md +630 -0
- package/docs/guide/views.md +278 -0
- package/docs/guide/void-integration.md +167 -0
- package/docs/index.md +4 -0
- package/lib/cli.mjs +17 -250
- package/lib/clientStub.d.mts +6 -0
- package/lib/clientStub.mjs +6 -0
- package/lib/executor-BqwHdN3n.d.mts +468 -0
- package/lib/index.d.mts +64 -3
- package/lib/index.mjs +1965 -559
- package/lib/list-4wRNSVgI.d.mts +10 -0
- package/lib/list-C_mi6GnE.mjs +16 -0
- package/lib/list.d.mts +2 -0
- package/lib/list.mjs +2 -0
- package/lib/liveTopics-DzNtJaBD.mjs +197 -0
- package/lib/{record-DnhZuvUe.mjs → record-AeZJC9fd.mjs} +1 -2
- package/lib/record-ENh92gyb.d.mts +315 -0
- package/lib/server/drizzle.d.mts +304 -0
- package/lib/server/drizzle.mjs +688 -0
- package/lib/server/prisma.d.mts +309 -0
- package/lib/server/prisma.mjs +345 -0
- package/lib/server.d.mts +82 -184
- package/lib/server.mjs +870 -416
- package/lib/sourceRouter-Bwpkr062.mjs +972 -0
- package/lib/{types-Djlh7mMH.d.mts → types-Dz46PXr3.d.mts} +327 -197
- package/lib/vite.d.mts +17 -0
- package/lib/vite.mjs +592 -0
- package/package.json +44 -6
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Views
|
|
2
|
+
|
|
3
|
+
## Defining Views
|
|
4
|
+
|
|
5
|
+
Let's start by defining a simple view for a blog's `Post` component. fate requires you to explicitly "select" each field that you plan to use in your components. Here is how you can define a view for a `Post` entity that has `title` and `content` fields:
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { view } from 'react-fate';
|
|
9
|
+
|
|
10
|
+
type Post = {
|
|
11
|
+
content: string;
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const PostView = view<Post>()({
|
|
17
|
+
content: true,
|
|
18
|
+
id: true,
|
|
19
|
+
title: true,
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Fields are selected by setting them to `true` in the view definition. This tells **_fate_** that these fields should be fetched from the server and made available to components that use this view.
|
|
24
|
+
|
|
25
|
+
> [!NOTE]
|
|
26
|
+
> The `Post` type above is an example. In a real application, this type is defined on the server and imported into your client code.
|
|
27
|
+
|
|
28
|
+
## Resolving a View with `useView`
|
|
29
|
+
|
|
30
|
+
Now we can use the view that we defined in a `PostCard` React component to resolve the data against a reference of an individual `Post`:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { useView, ViewRef } from 'react-fate';
|
|
34
|
+
|
|
35
|
+
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
36
|
+
const post = useView(PostView, postRef);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Card>
|
|
40
|
+
<h2>{post.title}</h2>
|
|
41
|
+
<p>{post.content}</p>
|
|
42
|
+
</Card>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A `ViewRef` is a reference to a concrete object of a specific type, for example a `Post` with id `7`. It contains the unique ID of the object, the type name (as `__typename`) and some fate-specific metadata. fate creates and manages these references for you, and you can pass them around your components as needed.
|
|
48
|
+
|
|
49
|
+
Components using `useView` listen to changes for all selected fields. When data changes, fate re-renders all of the fields that depend on that data. For example, if the `title` of the `Post` changes, the `PostCard` component re-renders with new data. However, if a different field such as `likes` that isn't selected in `PostView` changes, the `PostCard` component will not re-render.
|
|
50
|
+
|
|
51
|
+
## Fetching Data with `useRequest`
|
|
52
|
+
|
|
53
|
+
Now that we defined our view and component, we fetch the data from the server using the `useRequest` hook from fate. This hook allows us to declare what data we need for a specific screen or component tree. At the root of our app, we can request a list of posts like this:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { useRequest } from 'react-fate';
|
|
57
|
+
import { PostCard, PostView } from './PostCard.tsx';
|
|
58
|
+
|
|
59
|
+
export function App() {
|
|
60
|
+
const { posts } = useRequest({ posts: { list: PostView } });
|
|
61
|
+
|
|
62
|
+
return posts.map((post) => <PostCard key={post.id} post={post} />);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
_Learn more about `useRequest` in the [Requests Guide](requests.md)._
|
|
67
|
+
|
|
68
|
+
## Composing Views
|
|
69
|
+
|
|
70
|
+
In the above example we are defining a single view for a `Post`. One of fate's core strengths is view composition. Let's say we want to show the author's name along with the post. A simple way to do this is by adding an `author` field to the `PostView` with a concrete selection:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { Suspense } from 'react';
|
|
74
|
+
import { useView, ViewRef } from 'react-fate';
|
|
75
|
+
|
|
76
|
+
export const PostView = view<Post>()({
|
|
77
|
+
author: {
|
|
78
|
+
id: true,
|
|
79
|
+
name: true,
|
|
80
|
+
},
|
|
81
|
+
content: true,
|
|
82
|
+
id: true,
|
|
83
|
+
title: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const PostCard = ({ postRef }: { postRef: ViewRef<'Post'> }) => {
|
|
87
|
+
const post = useView(PostView, postRef);
|
|
88
|
+
return (
|
|
89
|
+
<Card>
|
|
90
|
+
<h2>{post.title}</h2>
|
|
91
|
+
<p>by {post.author.name}</p>
|
|
92
|
+
<p>{post.content}</p>
|
|
93
|
+
</Card>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This code fetches the author associated with the Post and makes it available to the `PostCard` component. However, this approach has some downsides:
|
|
99
|
+
|
|
100
|
+
1. The `author` selection is tightly coupled to the `PostView`. If we want to use the author's data in another component, we would need to duplicate the field selection.
|
|
101
|
+
1. If the `author` has more fields that we want to use in other components, we would need to add them to the `PostView`, leading to overfetching.
|
|
102
|
+
1. We cannot reuse the `author` field selection in other views or components.
|
|
103
|
+
|
|
104
|
+
In fate, views are composable and reusable. Instead of inlining the selection, we can define a `UserView` and compose it into the `PostView` like this:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import type { Post, User } from '@your-org/server/trpc/views';
|
|
108
|
+
import { view } from 'react-fate';
|
|
109
|
+
|
|
110
|
+
export const UserView = view<User>()({
|
|
111
|
+
id: true,
|
|
112
|
+
name: true,
|
|
113
|
+
profilePicture: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const PostView = view<Post>()({
|
|
117
|
+
author: UserView,
|
|
118
|
+
content: true,
|
|
119
|
+
id: true,
|
|
120
|
+
title: true,
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Now we can create a separate `UserCard` component that uses our `UserView`:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { useView, ViewRef } from 'react-fate';
|
|
128
|
+
|
|
129
|
+
export const UserCard = ({ user: userRef }: { user: ViewRef<'User'> }) => {
|
|
130
|
+
const user = useView(UserView, userRef);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div>
|
|
134
|
+
<img src={user.profilePicture} alt={user.name} />
|
|
135
|
+
<p>{user.name}</p>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
And update `PostCard` to use our `UserCard` component:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { UserCard } from './UserCard.tsx';
|
|
145
|
+
|
|
146
|
+
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
147
|
+
const post = useView(PostView, postRef);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Card>
|
|
151
|
+
<h2>{post.title}</h2>
|
|
152
|
+
<UserCard user={post.author} />
|
|
153
|
+
<p>{post.content}</p>
|
|
154
|
+
</Card>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## View Spreads
|
|
160
|
+
|
|
161
|
+
When building complex UIs, you will often build multiple components that share the same data requirements. In fate, you can use view spreads to compose such views together. This is similar to GraphQL fragment spreads, but works with plain JavaScript objects.
|
|
162
|
+
|
|
163
|
+
Let's assume we want to fetch and display additional information about the author in the `PostCard`, such as their bio. Instead of directly assigning our `UserView` to the `author` field, we can instead spread it and add the `bio` field:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
export const PostView = view<Post>()({
|
|
167
|
+
author: {
|
|
168
|
+
...UserView,
|
|
169
|
+
bio: true,
|
|
170
|
+
},
|
|
171
|
+
content: true,
|
|
172
|
+
id: true,
|
|
173
|
+
title: true,
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Now the `PostCard` component can access the `bio` field of the author:
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
181
|
+
const post = useView(PostView, postRef);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<Card>
|
|
185
|
+
<h2>{post.title}</h2>
|
|
186
|
+
<UserCard author={post.author} />
|
|
187
|
+
{/* Accessing the bio field */}
|
|
188
|
+
<p>{post.author.bio}</p>
|
|
189
|
+
<p>{post.content}</p>
|
|
190
|
+
</Card>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
We can also spread multiple views together. For example, if we have another view called `UserStatsView` that selects some statistics about the user, we can include it in the `PostView` like this:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
export const UserStatsView = view<User>()({
|
|
199
|
+
followerCount: true,
|
|
200
|
+
postCount: true,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
export const PostView = view<Post>()({
|
|
204
|
+
author: {
|
|
205
|
+
...UserView,
|
|
206
|
+
...UserStatsView,
|
|
207
|
+
bio: true,
|
|
208
|
+
},
|
|
209
|
+
content: true,
|
|
210
|
+
id: true,
|
|
211
|
+
title: true,
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Views are opaque objects. Even if you select the same field multiple times through different views, the composed object won't have conflicting fields or result in TypeScript errors. fate automatically deduplicates fields during runtime and ensures that each field is only fetched once.
|
|
216
|
+
|
|
217
|
+
## `useView` and Suspense
|
|
218
|
+
|
|
219
|
+
We learned that `useRequest` is responsible for fetching data from the server and `useView` is used for reading data from the cache. In some situations data may not be available in the cache and `useView` might need to suspend the component to fetch only the missing data. Once that data is fetched and written to the cache, the component resumes rendering.
|
|
220
|
+
|
|
221
|
+
_Tip: You can test this behavior in development mode with Fast Refresh (HMR) enabled in your bundler. When you edit the selection of a view, components using that view will suspend, fetch the missing data, and then resume rendering._
|
|
222
|
+
|
|
223
|
+
## Type Safety and Data Masking
|
|
224
|
+
|
|
225
|
+
fate provides guarantees through TypeScript and during runtime that prevent you from accessing data that wasn't selected in a component. This ensures that you declare all the data dependencies at the right level in your component tree, and prevents accidental coupling between components.
|
|
226
|
+
|
|
227
|
+
In the below example, we forgot to select the `content` of a `Post`. As a result, type-checks fail and the `content` field is undefined during runtime:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
const PostView = view<Post>()({
|
|
231
|
+
id: true,
|
|
232
|
+
title: true,
|
|
233
|
+
// `content: true` is omitted.
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
237
|
+
const post = useView(PostView, postRef);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<Card>
|
|
241
|
+
<h2>{post.title}</h2>
|
|
242
|
+
{/* TypeScript errors here, and `post.content` is undefined during runtime */}
|
|
243
|
+
<p>{post.content}</p>
|
|
244
|
+
</Card>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Views can only be resolved against refs that include that view directly or via view spreads. If a component tries to resolve a view against a ref that isn't linked, it will throw an error during runtime:
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
const PostDetailView = view<Post>()({
|
|
253
|
+
content: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const AnotherPostView = view<Post>()({
|
|
257
|
+
content: true,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const PostView = view<Post>()({
|
|
261
|
+
id: true,
|
|
262
|
+
title: true,
|
|
263
|
+
...AnotherPostView,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
267
|
+
const post = useView(PostView, postRef);
|
|
268
|
+
return <PostDetail post={post} />;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const PostDetail = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
272
|
+
// This throws because the post reference passed into this component
|
|
273
|
+
// is of type `AnotherPostView`, not `PostDetailView`.
|
|
274
|
+
const post = useView(PostDetailView, postRef);
|
|
275
|
+
};
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
ViewRefs carry a set of view names they can resolve. `useView` throws if a ref does not include the required view.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Void Integration
|
|
2
|
+
|
|
3
|
+
`void-fate` is the first-class [Void](https://void.cloud) adapter for fate to ease integration with the Void SDK and for deploying to the Void platform.
|
|
4
|
+
|
|
5
|
+
Use this integration when your app runs on Void and you want the example app's
|
|
6
|
+
setup without copying its adapter glue.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @nkzw/fate react-fate void-fate void
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Vite
|
|
15
|
+
|
|
16
|
+
Use the regular `react-fate` Vite plugin with the Void transport:
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { voidReact } from '@void/react/plugin';
|
|
20
|
+
import { fate } from 'react-fate/vite';
|
|
21
|
+
import { defineConfig } from 'vite-plus';
|
|
22
|
+
import { voidPlugin } from 'void';
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
plugins: [
|
|
26
|
+
voidPlugin(),
|
|
27
|
+
voidReact(),
|
|
28
|
+
fate({
|
|
29
|
+
module: './src/fate/server.ts',
|
|
30
|
+
transport: 'void',
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The generated client uses `/fate` for RPC requests and `/fate-live` for live
|
|
37
|
+
updates by default. In SSR, it calls the exported fate server directly. In the
|
|
38
|
+
browser, it uses fetch and the SSE live endpoint.
|
|
39
|
+
|
|
40
|
+
## Server Setup
|
|
41
|
+
|
|
42
|
+
Create a Void live adapter with `createVoidFateLive`, pass its `live` event bus
|
|
43
|
+
to `createFateServer`, and export the adapter next to your fate server.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { createFateServer } from '@nkzw/fate/server';
|
|
47
|
+
import { createDrizzleSourceAdapter } from '@nkzw/fate/server/drizzle';
|
|
48
|
+
import { createVoidFateLive } from 'void-fate/server';
|
|
49
|
+
import { db } from 'void/db';
|
|
50
|
+
import schema from '../db/schema.ts';
|
|
51
|
+
import { createContext } from './context.ts';
|
|
52
|
+
import { Root } from './views.ts';
|
|
53
|
+
|
|
54
|
+
const sources = createDrizzleSourceAdapter({
|
|
55
|
+
db,
|
|
56
|
+
schema,
|
|
57
|
+
views: Root,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const fateLive = createVoidFateLive();
|
|
61
|
+
export const { live } = fateLive;
|
|
62
|
+
|
|
63
|
+
export const fateServer = createFateServer({
|
|
64
|
+
context: ({ request }) => createContext({ request }),
|
|
65
|
+
live,
|
|
66
|
+
roots: Root,
|
|
67
|
+
sources,
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Your app can publish live updates through the normal fate live bus:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
live.update('Post', postId, { changed: ['likes'] });
|
|
75
|
+
live.connection('Post.comments', { id: postId }).appendNode('Comment', commentId, {
|
|
76
|
+
node: comment,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`changed` is optional. Void still uses generic topic fanout, while fate uses the changed field paths to refetch or write only the selected fields affected by the event.
|
|
81
|
+
|
|
82
|
+
## Routes
|
|
83
|
+
|
|
84
|
+
Add one route for fate RPC requests:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// routes/fate.ts
|
|
88
|
+
import { defineVoidFateRoute } from 'void-fate/server';
|
|
89
|
+
import { fateLive, fateServer } from '../src/fate/server.ts';
|
|
90
|
+
|
|
91
|
+
export const { GET, POST } = defineVoidFateRoute(fateServer, fateLive);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Add a second route for the live SSE transport:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// routes/fate-live.ts
|
|
98
|
+
import { defineVoidFateLiveRoute } from 'void-fate/server';
|
|
99
|
+
import { fateLive, fateServer } from '../src/fate/server.ts';
|
|
100
|
+
|
|
101
|
+
export const { GET, POST } = defineVoidFateLiveRoute(fateServer, fateLive);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The live route handles `GET /fate-live` SSE connections and `POST /fate-live`
|
|
105
|
+
control messages. `void-fate` does not use WebSockets.
|
|
106
|
+
|
|
107
|
+
## React Layout
|
|
108
|
+
|
|
109
|
+
Wrap your app with `VoidFateClient` from `void-fate/react`. It creates the
|
|
110
|
+
generated fate client and provides it through `react-fate`:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { useShared } from '@void/react';
|
|
114
|
+
import type { ReactNode } from 'react';
|
|
115
|
+
import { VoidFateClient } from 'void-fate/react';
|
|
116
|
+
import type { SharedData } from '../src/lib/shared.ts';
|
|
117
|
+
|
|
118
|
+
export default function Layout({ children }: { children: ReactNode }) {
|
|
119
|
+
const shared = useShared<SharedData>();
|
|
120
|
+
const userId = shared.auth.user?.id;
|
|
121
|
+
const origin = typeof window === 'undefined' ? shared.origin : window.location.origin;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<VoidFateClient origin={origin} userId={userId}>
|
|
125
|
+
{children}
|
|
126
|
+
</VoidFateClient>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`userId` is optional, but passing it lets `VoidFateClient` recreate the client
|
|
132
|
+
when the signed-in user changes. Browser requests include credentials when a
|
|
133
|
+
`userId` is present.
|
|
134
|
+
|
|
135
|
+
## Custom Paths
|
|
136
|
+
|
|
137
|
+
The default route pair is `/fate` and `/fate-live`. If your Void app uses
|
|
138
|
+
different paths, configure the same values on the live adapter and client.
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
export const fateLive = createVoidFateLive({
|
|
142
|
+
livePath: '/custom-fate-live',
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<VoidFateClient livePath="/custom-fate-live" origin={origin} rpcPath="/custom-fate" userId={userId}>
|
|
148
|
+
{children}
|
|
149
|
+
</VoidFateClient>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The route helper does not own the route path. Make sure your Void route filename
|
|
153
|
+
or router configuration matches the paths you pass to the client.
|
|
154
|
+
|
|
155
|
+
## Live Transport
|
|
156
|
+
|
|
157
|
+
Void can run separate request handlers for mutations and long-lived SSE
|
|
158
|
+
connections. `createVoidFateLive` bridges those handlers by publishing live
|
|
159
|
+
events from the request that changed data to the live route.
|
|
160
|
+
|
|
161
|
+
In local development, `void-fate` uses a development token for that internal
|
|
162
|
+
publish request. Outside local development, Void must provide `__VOID_PROXY_TOKEN`
|
|
163
|
+
in the route environment. If no internal publish token is available, the adapter
|
|
164
|
+
falls back to the in-memory live bus for the current request context.
|
|
165
|
+
|
|
166
|
+
The live transport is best-effort and does not replay missed events after a
|
|
167
|
+
client reconnects. This matches fate's default in-memory live event bus.
|
package/docs/index.md
ADDED