@nkzw/fate 1.0.3 → 1.2.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 +1220 -147
- package/docs/api/classes/FateClient.md +134 -40
- package/docs/api/functions/clientRoot.md +1 -1
- package/docs/api/functions/createClient.md +8 -4
- package/docs/api/functions/createGraphQLTransport.md +21 -0
- package/docs/api/functions/createHTTPTransport.md +1 -1
- package/docs/api/functions/createTRPCTransport.md +1 -1
- package/docs/api/functions/defer.md +21 -0
- package/docs/api/functions/getDeferredMetadata.md +21 -0
- package/docs/api/functions/getListEntries.md +1 -1
- package/docs/api/functions/getSelectionPlan.md +1 -1
- package/docs/api/functions/graphqlMutation.md +39 -0
- package/docs/api/functions/isDeferred.md +15 -0
- package/docs/api/functions/isRecord.md +1 -1
- package/docs/api/functions/isViewTag.md +1 -1
- package/docs/api/functions/liveConnectionTopic.md +1 -1
- package/docs/api/functions/liveEntityTopic.md +1 -1
- package/docs/api/functions/liveGlobalConnectionTopic.md +1 -1
- package/docs/api/functions/mutation.md +1 -1
- package/docs/api/functions/toEntityId.md +1 -1
- package/docs/api/functions/view.md +1 -1
- package/docs/api/index.md +19 -0
- package/docs/api/interfaces/FateThenable.md +3 -3
- package/docs/api/interfaces/Transport.md +7 -7
- package/docs/api/server/classes/FateRequestError.md +5 -5
- package/docs/api/server/drizzle/functions/createDrizzleFate.md +1 -1
- package/docs/api/server/drizzle/functions/createDrizzleSourceAdapter.md +1 -1
- package/docs/api/server/drizzle/functions/createDrizzleSourceRegistry.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyConfig.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyInput.md +1 -1
- package/docs/api/server/drizzle/type-aliases/DrizzleQueryExtra.md +3 -3
- package/docs/api/server/drizzle/type-aliases/DrizzleSourceAdapter.md +9 -9
- package/docs/api/server/drizzle/type-aliases/DrizzleViewConfig.md +1 -1
- package/docs/api/server/functions/bindSourceProcedures.md +1 -1
- package/docs/api/server/functions/computed.md +1 -1
- package/docs/api/server/functions/count.md +1 -1
- package/docs/api/server/functions/createFateFetchHandler.md +1 -1
- package/docs/api/server/functions/createFateServer.md +1 -1
- package/docs/api/server/functions/createHonoFateHandler.md +1 -1
- package/docs/api/server/functions/createLiveEventBus.md +1 -1
- package/docs/api/server/functions/createNestedSourcePlan.md +1 -1
- package/docs/api/server/functions/createResolver.md +1 -1
- package/docs/api/server/functions/createSourcePlan.md +1 -1
- package/docs/api/server/functions/createSourceProcedures.md +1 -1
- package/docs/api/server/functions/dataView.md +1 -1
- package/docs/api/server/functions/field.md +1 -1
- package/docs/api/server/functions/getNestedSelection.md +1 -1
- package/docs/api/server/functions/getScopedArgs.md +1 -1
- package/docs/api/server/functions/hasNestedSelection.md +1 -1
- package/docs/api/server/functions/list.md +1 -1
- package/docs/api/server/functions/refetchSourceById.md +1 -1
- package/docs/api/server/functions/resolveSourceById.md +1 -1
- package/docs/api/server/functions/resolveSourceByIds.md +1 -1
- package/docs/api/server/functions/resolveSourceConnection.md +1 -1
- package/docs/api/server/functions/resolver.md +1 -1
- package/docs/api/server/functions/toPrismaSelect.md +1 -1
- package/docs/api/server/functions/withConnection.md +1 -1
- package/docs/api/server/prisma/functions/createPrismaFate.md +1 -1
- package/docs/api/server/prisma/functions/createPrismaSourceAdapter.md +1 -1
- package/docs/api/server/prisma/functions/createPrismaSourceRegistry.md +1 -1
- package/docs/api/server/prisma/functions/prismaConnectionArgs.md +1 -1
- package/docs/api/server/prisma/type-aliases/PrismaDelegate.md +4 -4
- package/docs/api/server/prisma/type-aliases/PrismaQueryExtra.md +1 -1
- package/docs/api/server/prisma/type-aliases/PrismaSourceAdapter.md +9 -9
- package/docs/api/server/prisma/type-aliases/PrismaViewConfig.md +1 -1
- package/docs/api/server/type-aliases/ComputedField.md +5 -5
- package/docs/api/server/type-aliases/ComputedSelection.md +1 -1
- package/docs/api/server/type-aliases/ConnectionItem.md +3 -3
- package/docs/api/server/type-aliases/ConnectionPagination.md +5 -5
- package/docs/api/server/type-aliases/ConnectionResult.md +3 -3
- package/docs/api/server/type-aliases/CountSelection.md +4 -4
- package/docs/api/server/type-aliases/CountWhere.md +1 -1
- package/docs/api/server/type-aliases/DataViewListOptions.md +2 -2
- package/docs/api/server/type-aliases/DataViewOrderBy.md +1 -1
- package/docs/api/server/type-aliases/DataViewOrderDirection.md +1 -1
- package/docs/api/server/type-aliases/DataViewResult.md +1 -1
- package/docs/api/server/type-aliases/Entity.md +1 -1
- package/docs/api/server/type-aliases/FateServer.md +5 -5
- package/docs/api/server/type-aliases/FateServerManifest.md +1 -1
- package/docs/api/server/type-aliases/FieldSelection.md +3 -3
- package/docs/api/server/type-aliases/LiveConnectionEventType.md +1 -1
- package/docs/api/server/type-aliases/LiveConnectionSourceEvent.md +1 -1
- package/docs/api/server/type-aliases/LiveEventBus.md +1 -1
- package/docs/api/server/type-aliases/LiveEventType.md +1 -1
- package/docs/api/server/type-aliases/LiveSourceEvent.md +1 -1
- package/docs/api/server/type-aliases/NativeFateAPI.md +4 -4
- package/docs/api/server/type-aliases/OrderDirection.md +1 -1
- package/docs/api/server/type-aliases/SourceConfig.md +6 -6
- package/docs/api/server/type-aliases/SourceDefinition.md +6 -6
- package/docs/api/server/type-aliases/SourceOrder.md +1 -1
- package/docs/api/server/type-aliases/SourceOrderField.md +3 -3
- package/docs/api/server/type-aliases/SourcePlan.md +1 -1
- package/docs/api/server/type-aliases/SourcePlanNode.md +1 -1
- package/docs/api/server/type-aliases/SourceRegistry.md +1 -1
- package/docs/api/server/type-aliases/SourceRelation.md +1 -1
- package/docs/api/server/type-aliases/SourceRelationConfig.md +1 -1
- package/docs/api/server/variables/byIdInput.md +1 -1
- package/docs/api/server/variables/connectionArgs.md +1 -1
- package/docs/api/type-aliases/ConnectionMetadata.md +1 -1
- package/docs/api/type-aliases/ConnectionRef.md +1 -1
- package/docs/api/type-aliases/Deferred.md +19 -0
- package/docs/api/type-aliases/DeferredMetadata.md +7 -0
- package/docs/api/type-aliases/DeferredSelection.md +13 -0
- package/docs/api/type-aliases/DeferredSnapshot.md +11 -0
- package/docs/api/type-aliases/Entity.md +2 -2
- package/docs/api/type-aliases/EntityId.md +1 -1
- package/docs/api/type-aliases/FateDehydratedState.md +11 -0
- package/docs/api/type-aliases/FateLiveConnectionEvent.md +1 -1
- package/docs/api/type-aliases/FateLiveEvent.md +1 -1
- package/docs/api/type-aliases/FateMutations.md +1 -1
- package/docs/api/type-aliases/FateOperation.md +1 -1
- package/docs/api/type-aliases/FateProtocolRequest.md +1 -1
- package/docs/api/type-aliases/FateProtocolResponse.md +1 -1
- package/docs/api/type-aliases/FateRecord.md +1 -1
- package/docs/api/type-aliases/FateRoots.md +1 -1
- package/docs/api/type-aliases/GraphQLMutationDefinition.md +19 -0
- package/docs/api/type-aliases/GraphQLMutationInput.md +11 -0
- package/docs/api/type-aliases/GraphQLMutationMap.md +11 -0
- package/docs/api/type-aliases/GraphQLMutationOutput.md +11 -0
- package/docs/api/type-aliases/GraphQLTransportOptions.md +119 -0
- package/docs/api/type-aliases/HydrateOptions.md +7 -0
- package/docs/api/type-aliases/HydrationLimits.md +7 -0
- package/docs/api/type-aliases/InferFateAPI.md +1 -1
- package/docs/api/type-aliases/List.md +1 -1
- package/docs/api/type-aliases/ListEntry.md +1 -1
- package/docs/api/type-aliases/ListItem.md +1 -1
- package/docs/api/type-aliases/Mask.md +1 -1
- package/docs/api/type-aliases/MutationDefinition.md +1 -1
- package/docs/api/type-aliases/MutationEntity.md +1 -1
- package/docs/api/type-aliases/MutationIdentifier.md +1 -1
- package/docs/api/type-aliases/MutationInput.md +1 -1
- package/docs/api/type-aliases/MutationResult.md +1 -1
- package/docs/api/type-aliases/NodesItem.md +1 -1
- package/docs/api/type-aliases/Pagination.md +5 -5
- package/docs/api/type-aliases/Request.md +1 -1
- package/docs/api/type-aliases/RequestMode.md +1 -1
- package/docs/api/type-aliases/RequestOptions.md +1 -1
- package/docs/api/type-aliases/RequestResult.md +1 -1
- package/docs/api/type-aliases/Selection.md +1 -1
- package/docs/api/type-aliases/Snapshot.md +1 -1
- package/docs/api/type-aliases/TypeConfig.md +4 -4
- package/docs/api/type-aliases/View.md +1 -1
- package/docs/api/type-aliases/ViewData.md +1 -1
- package/docs/api/type-aliases/ViewEntity.md +1 -1
- package/docs/api/type-aliases/ViewEntityName.md +1 -1
- package/docs/api/type-aliases/ViewRef.md +1 -1
- package/docs/api/type-aliases/ViewSelection.md +1 -1
- package/docs/api/type-aliases/ViewSnapshot.md +1 -1
- package/docs/api/type-aliases/ViewTag.md +1 -1
- package/docs/api/variables/ConnectionTag.md +1 -1
- package/docs/api/variables/DeferTag.md +7 -0
- package/docs/api/variables/DeferredTag.md +7 -0
- package/docs/guide/actions.md +7 -7
- package/docs/guide/deferred-views.md +62 -0
- package/docs/guide/getting-started.md +21 -3
- package/docs/guide/requests.md +52 -0
- package/docs/guide/vue.md +256 -0
- package/docs/index.md +1 -0
- package/docs/integrations/graphql.md +345 -0
- package/docs/{guide/server-integration.md → integrations/server.md} +94 -7
- package/docs/{guide/void-integration.md → integrations/void.md} +94 -12
- package/lib/{executor-BqwHdN3n.d.mts → executor-Dh-MLUXc.d.mts} +1 -1
- package/lib/graphqlTransport-B3cbhvJX.d.mts +74 -0
- package/lib/graphqlTransport-IFgmY-Qq.mjs +499 -0
- package/lib/graphqlTransport.d.mts +2 -0
- package/lib/graphqlTransport.mjs +2 -0
- package/lib/index.d.mts +10 -4
- package/lib/index.mjs +619 -91
- package/lib/{list-4wRNSVgI.d.mts → list-BrL6PD_6.d.mts} +1 -1
- package/lib/list.d.mts +1 -1
- package/lib/list.mjs +1 -1
- package/lib/{liveTopics-DzNtJaBD.mjs → liveTopics-JIDh3t3P.mjs} +1 -1
- package/lib/{record-CirbfZWX.d.mts → record-BkvZwvFk.d.mts} +2 -2
- package/lib/server/drizzle.d.mts +6 -8
- package/lib/server/drizzle.mjs +2 -2
- package/lib/server/prisma.d.mts +6 -8
- package/lib/server/prisma.mjs +2 -2
- package/lib/server.d.mts +3 -3
- package/lib/server.mjs +3 -3
- package/lib/{sourceRouter-C22uFcZg.mjs → sourceRouter-N9Vp0Cj6.mjs} +1 -1
- package/lib/{types-Dz46PXr3.d.mts → transport-BjGm__3t.d.mts} +246 -145
- package/lib/vite.d.mts +2 -2
- package/lib/vite.mjs +150 -3
- package/package.json +11 -3
- /package/lib/{list-C_mi6GnE.mjs → list-_6fXYLtJ.mjs} +0 -0
- /package/lib/{record-AeZJC9fd.mjs → record-B07VwXd-.mjs} +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- auto-generated from docs/guide/*.md. Do not edit directly. -->
|
|
1
|
+
<!-- auto-generated from docs/guide/*.md and docs/integrations/*.md. Do not edit directly. -->
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<picture>
|
|
@@ -68,11 +68,11 @@ Create a new fate app with Vite+:
|
|
|
68
68
|
vp create fate my-app
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
The template selector can create a Void app with Drizzle, a tRPC app with Drizzle
|
|
71
|
+
The template selector can create a React or Vue client for a Void app with Drizzle, a tRPC app with Drizzle or Prisma, a GraphQL app with Prisma, or a fate client for an existing GraphQL server. React is the default UI framework; pass `--framework vue` or choose Vue in the template selector to create a Vue app. The template sources live in the fate repo under [`packages/create-fate/templates/fate`](https://github.com/nkzw-tech/fate/tree/main/packages/create-fate/templates/fate). They feature modern tools to deliver an incredibly fast development experience.
|
|
72
72
|
|
|
73
73
|
### Manual Installation
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
For a React client, install `react-fate`. It requires React 19.2+:
|
|
76
76
|
|
|
77
77
|
::: code-group
|
|
78
78
|
|
|
@@ -90,7 +90,25 @@ yarn add react-fate
|
|
|
90
90
|
|
|
91
91
|
:::
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
For a Vue client, install `vue-fate`:
|
|
94
|
+
|
|
95
|
+
::: code-group
|
|
96
|
+
|
|
97
|
+
```bash [npm]
|
|
98
|
+
npm add vue-fate
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```bash [pnpm]
|
|
102
|
+
pnpm add vue-fate
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```bash [yarn]
|
|
106
|
+
yarn add vue-fate
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
:::
|
|
110
|
+
|
|
111
|
+
If your server is a separate package, install `@nkzw/fate` there as a runtime dependency too. Install `@nkzw/fate` on the client only for a barebones integration without a framework adapter:
|
|
94
112
|
|
|
95
113
|
::: code-group
|
|
96
114
|
|
|
@@ -549,6 +567,121 @@ try {
|
|
|
549
567
|
|
|
550
568
|
Garbage collection waits for active optimistic updates to settle before sweeping records. This keeps temporary optimistic records and their list positions stable while mutations are still pending.
|
|
551
569
|
|
|
570
|
+
### SSR and Hydration
|
|
571
|
+
|
|
572
|
+
Create a request-scoped fate client on the server, preload the route data, and dehydrate its normalized cache:
|
|
573
|
+
|
|
574
|
+
```tsx
|
|
575
|
+
const fate = createFateClient();
|
|
576
|
+
await fate.request({ post: { id: '12', view: PostView } });
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
fate: fate.dehydrate(),
|
|
580
|
+
};
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Transport the returned value through your framework's loader serialization, React Server Component props, or a safely escaped JSON bootstrap script. The snapshot contains plain serializable values, so serializers such as Seroval can carry it without fate-specific integration. Treat the snapshot as opaque: hydrate it through fate rather than reading or editing its internal data.
|
|
584
|
+
|
|
585
|
+
On the browser, hydrate the new client before rendering components that call `useRequest`:
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
const fate = createFateClient();
|
|
589
|
+
fate.hydrate(loaderData.fate);
|
|
590
|
+
|
|
591
|
+
hydrateRoot(
|
|
592
|
+
document,
|
|
593
|
+
<FateClient client={fate}>
|
|
594
|
+
<App />
|
|
595
|
+
</FateClient>,
|
|
596
|
+
);
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
Hydrated `cache-first` requests resolve from the normalized cache without refetching. Hydration restores records, selected-field coverage, root queries, and list pagination state. It intentionally does not restore active requests, subscriptions, retainers, timers, or optimistic mutation state.
|
|
600
|
+
|
|
601
|
+
Snapshots carry a hydration scope and are rejected by clients with a different scope. Generated clients set a stable scope automatically. When constructing a client directly, pass `hydrationScope` and rotate it when deploying an incompatible cache schema or when separating cache namespaces:
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
const fate = createClient({
|
|
605
|
+
hydrationScope: 'storefront-v2',
|
|
606
|
+
// ...
|
|
607
|
+
});
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Use `hydrationLimits` when an application needs stricter bootstrap payload limits. fate applies conservative defaults for total encoded values, collection sizes, and string lengths.
|
|
611
|
+
|
|
612
|
+
By default, hydration preserves values already present in the browser cache while adding missing server data. Pass `{ merge: 'replace' }` only when the snapshot should authoritatively reset the durable cache:
|
|
613
|
+
|
|
614
|
+
```tsx
|
|
615
|
+
fate.hydrate(loaderData.fate, { merge: 'replace' });
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
`preserve-existing` recursively combines plain scalar objects while keeping browser values on conflicts. Arrays, dates, entity references, and list windows are atomic: an existing browser value wins as a whole. Replaying a snapshot is safe and does not notify subscribers when durable cache state is unchanged.
|
|
619
|
+
|
|
620
|
+
Do not reuse request-scoped snapshots across users. Dehydrate after awaited route preloading: snapshots are point-in-time values and do not stream cache patches for data that resolves later. Hydration and dehydration reject clients with in-flight requests, so hydrate the initial snapshot before rendering.
|
|
621
|
+
|
|
622
|
+
## Deferred Views
|
|
623
|
+
|
|
624
|
+
Use `defer` when a field should not block the parent view. The parent view receives a deferred handle immediately after the eager fields are available, and the component that reads that handle with `useView`, `useListView`, or `useLiveListView` decides which `Suspense` boundary handles the loading state.
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
import { Suspense } from 'react';
|
|
628
|
+
import { defer, useListView, useView, view, Deferred, ViewRef } from 'react-fate';
|
|
629
|
+
|
|
630
|
+
const CommentView = view<Comment>()({
|
|
631
|
+
content: true,
|
|
632
|
+
id: true,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const CommentConnectionView = {
|
|
636
|
+
args: { first: 3 },
|
|
637
|
+
items: { node: CommentView },
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const PostView = view<Post>()({
|
|
641
|
+
comments: defer(CommentConnectionView),
|
|
642
|
+
content: true,
|
|
643
|
+
id: true,
|
|
644
|
+
title: true,
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
function PostCard({ post: postRef }: { post: ViewRef<'Post'> }) {
|
|
648
|
+
const post = useView(PostView, postRef);
|
|
649
|
+
|
|
650
|
+
return (
|
|
651
|
+
<article>
|
|
652
|
+
<h2>{post.title}</h2>
|
|
653
|
+
<p>{post.content}</p>
|
|
654
|
+
<Suspense fallback={<CommentsSkeleton />}>
|
|
655
|
+
<PostComments comments={post.comments} />
|
|
656
|
+
</Suspense>
|
|
657
|
+
</article>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function PostComments({
|
|
662
|
+
comments,
|
|
663
|
+
}: {
|
|
664
|
+
comments: Deferred<{ items: ReadonlyArray<{ node: ViewRef<'Comment'> }> }>;
|
|
665
|
+
}) {
|
|
666
|
+
const [items, loadNext] = useListView(CommentConnectionView, comments);
|
|
667
|
+
|
|
668
|
+
return (
|
|
669
|
+
<section>
|
|
670
|
+
{items.map(({ node }) => (
|
|
671
|
+
<CommentCard comment={node} key={node.id} />
|
|
672
|
+
))}
|
|
673
|
+
{loadNext ? <button onClick={loadNext}>Load more</button> : null}
|
|
674
|
+
</section>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Deferred fields are not optional data. They are explicit handles that existing view APIs can read. If the deferred selection is missing from the normalized cache, fate fetches only that missing selection and suspends the component that tried to resolve it.
|
|
680
|
+
|
|
681
|
+
This keeps parent components simple: eager fields like `title` and `content` are available when `useView(PostView, postRef)` returns, while slower or secondary fields such as `comments` can load under their own boundary.
|
|
682
|
+
|
|
683
|
+
GraphQL transports use the same client semantics today. The deferred field is omitted from the eager request and fetched when the deferred handle is resolved. GraphQL `@defer` is the natural transport representation for this feature, but consuming incremental multipart patches requires additional transport support before fate can safely normalize streamed patches from a single GraphQL response.
|
|
684
|
+
|
|
552
685
|
## List Views
|
|
553
686
|
|
|
554
687
|
### Pagination with `useListView`
|
|
@@ -904,9 +1037,9 @@ fate does not provide hooks for mutations like traditional data fetching librari
|
|
|
904
1037
|
|
|
905
1038
|
Server mutations are exposed automatically as actions and mutations by fate's Vite plugin. The transport determines where those mutations are declared:
|
|
906
1039
|
|
|
907
|
-
- With the [native HTTP transport](/docs/
|
|
908
|
-
- With the [tRPC adapter](/docs/
|
|
909
|
-
- With [Void](/docs/
|
|
1040
|
+
- With the [native HTTP transport](/docs/integrations/server.md#native-fate-protocol), mutations come from the `mutations` object passed to `createFateServer`.
|
|
1041
|
+
- With the [tRPC adapter](/docs/integrations/server.md#trpc-fate-setup), mutations come from tRPC mutation procedures exposed through your fate-enabled router.
|
|
1042
|
+
- With [Void](/docs/integrations/void.md), mutations use the same native fate server shape and are exposed through the Void route helpers.
|
|
910
1043
|
|
|
911
1044
|
If you have a mutation named `post.like`, a `LikeButton` component using fate Actions and an async component library could look like this:
|
|
912
1045
|
|
|
@@ -1045,9 +1178,9 @@ You can call mutations from anywhere, and without waiting for previous mutations
|
|
|
1045
1178
|
|
|
1046
1179
|
fate Actions & Mutations are backed by regular server mutations. If you already know how your fate server is wired, the client-side API above is the same regardless of transport. If not, start with the server setup for your environment:
|
|
1047
1180
|
|
|
1048
|
-
- [Native HTTP custom mutations](/docs/
|
|
1049
|
-
- [tRPC fate setup](/docs/
|
|
1050
|
-
- [Void integration](/docs/
|
|
1181
|
+
- [Native HTTP custom mutations](/docs/integrations/server.md#custom-mutations) use `createFateServer({ mutations })`.
|
|
1182
|
+
- [tRPC fate setup](/docs/integrations/server.md#trpc-fate-setup) wires fate into your tRPC router; custom writes can use the same `fate.createPlan` and `fate.resolveById` helpers shown there.
|
|
1183
|
+
- [Void integration](/docs/integrations/void.md) exposes a native fate server from Void routes; define mutations with the native `createFateServer({ mutations })` API and serve them through `defineVoidFateRoute`.
|
|
1051
1184
|
|
|
1052
1185
|
Here is a native HTTP mutation for `post.like`:
|
|
1053
1186
|
|
|
@@ -1120,7 +1253,7 @@ export const postRouter = router({
|
|
|
1120
1253
|
});
|
|
1121
1254
|
```
|
|
1122
1255
|
|
|
1123
|
-
See [Server Integration](/docs/
|
|
1256
|
+
See [Server Integration](/docs/integrations/server.md) for complete native HTTP and tRPC setup examples, and [Void Integration](/docs/integrations/void.md) for route helpers when your app runs on Void.
|
|
1124
1257
|
|
|
1125
1258
|
### Action & Mutation Error Handling
|
|
1126
1259
|
|
|
@@ -1201,198 +1334,820 @@ addComment({
|
|
|
1201
1334
|
});
|
|
1202
1335
|
```
|
|
1203
1336
|
|
|
1204
|
-
##
|
|
1205
|
-
|
|
1206
|
-
Until now, we have focused on the client-side API of fate. You'll need a backend that follows fate's data protocol so the Vite plugin can wire the typed fate APIs into your app. _fate_ currently ships two server paths:
|
|
1337
|
+
## Vue
|
|
1207
1338
|
|
|
1208
|
-
|
|
1209
|
-
- The tRPC adapter, which keeps compatibility with existing tRPC backends.
|
|
1339
|
+
_fate_ also supports Vue through `vue-fate`. It exports the same core primitives as `react-fate` where Vue has a natural equivalent: `view`, `useRequest`, `useView`, `useListView`, `useLiveView`, `useLiveListView`, `useFateClient`, and the `FateClient` provider.
|
|
1210
1340
|
|
|
1211
|
-
|
|
1341
|
+
Vue components use fate through Vue resources built from refs, computed values, watchers, and `<Suspense>`. The view model, generated client, normalized cache, masking, request shapes, list views, live views, and mutations are shared with the React adapter.
|
|
1212
1342
|
|
|
1213
|
-
###
|
|
1343
|
+
### Installation
|
|
1214
1344
|
|
|
1215
|
-
fate
|
|
1345
|
+
Install `vue-fate` in your Vue client:
|
|
1216
1346
|
|
|
1217
|
-
-
|
|
1218
|
-
- A `list` query for fetching lists of objects with support for pagination.
|
|
1347
|
+
::: code-group
|
|
1219
1348
|
|
|
1220
|
-
|
|
1349
|
+
```bash [npm]
|
|
1350
|
+
npm add vue-fate
|
|
1351
|
+
```
|
|
1221
1352
|
|
|
1222
|
-
|
|
1353
|
+
```bash [pnpm]
|
|
1354
|
+
pnpm add vue-fate
|
|
1355
|
+
```
|
|
1223
1356
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1357
|
+
```bash [yarn]
|
|
1358
|
+
yarn add vue-fate
|
|
1359
|
+
```
|
|
1226
1360
|
|
|
1227
|
-
|
|
1361
|
+
:::
|
|
1228
1362
|
|
|
1229
|
-
|
|
1363
|
+
If your server lives in a separate package, install `@nkzw/fate` there as a runtime dependency too.
|
|
1230
1364
|
|
|
1231
|
-
|
|
1365
|
+
### Vite Plugin
|
|
1232
1366
|
|
|
1233
|
-
|
|
1367
|
+
Use the Vue adapter's Vite plugin in the client app:
|
|
1234
1368
|
|
|
1235
|
-
```
|
|
1236
|
-
import {
|
|
1237
|
-
import
|
|
1369
|
+
```ts
|
|
1370
|
+
import { fate } from 'vue-fate/vite';
|
|
1371
|
+
import { defineConfig } from 'vite';
|
|
1372
|
+
import vue from '@vitejs/plugin-vue';
|
|
1238
1373
|
|
|
1239
|
-
export
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1374
|
+
export default defineConfig({
|
|
1375
|
+
plugins: [
|
|
1376
|
+
vue(),
|
|
1377
|
+
fate({
|
|
1378
|
+
module: '@your-org/server/fate.ts',
|
|
1379
|
+
transport: 'native',
|
|
1380
|
+
}),
|
|
1381
|
+
],
|
|
1243
1382
|
});
|
|
1244
|
-
|
|
1245
|
-
export type User = Entity<typeof userDataView, 'User'>;
|
|
1246
1383
|
```
|
|
1247
1384
|
|
|
1248
|
-
|
|
1249
|
-
import { dataView, type Entity } from '@nkzw/fate/server';
|
|
1250
|
-
import type { UserRow } from '../drizzle/schema.ts';
|
|
1385
|
+
The plugin generates `vue-fate/client`, which contains the typed `createFateClient` helper for your app. The `module` and `transport` options are the same options used by the React adapter.
|
|
1251
1386
|
|
|
1252
|
-
|
|
1253
|
-
id: true,
|
|
1254
|
-
name: true,
|
|
1255
|
-
username: true,
|
|
1256
|
-
});
|
|
1387
|
+
### Providing the Client
|
|
1257
1388
|
|
|
1258
|
-
|
|
1259
|
-
```
|
|
1389
|
+
Create a client with `createFateClient` and provide it with `FateClient`:
|
|
1260
1390
|
|
|
1261
|
-
|
|
1391
|
+
```vue
|
|
1392
|
+
<script setup lang="ts">
|
|
1393
|
+
import { computed, ref } from 'vue';
|
|
1394
|
+
import { FateClient } from 'vue-fate';
|
|
1395
|
+
import { createFateClient } from 'vue-fate/client';
|
|
1396
|
+
import AppRoutes from './AppRoutes.vue';
|
|
1262
1397
|
|
|
1263
|
-
|
|
1398
|
+
const token = ref<string | null>(null);
|
|
1264
1399
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
})
|
|
1400
|
+
const fate = computed(() =>
|
|
1401
|
+
createFateClient({
|
|
1402
|
+
headers: () => ({
|
|
1403
|
+
authorization: token.value ? `Bearer ${token.value}` : '',
|
|
1404
|
+
}),
|
|
1405
|
+
url: '/fate',
|
|
1406
|
+
}),
|
|
1407
|
+
);
|
|
1408
|
+
</script>
|
|
1409
|
+
|
|
1410
|
+
<template>
|
|
1411
|
+
<FateClient :client="fate">
|
|
1412
|
+
<Suspense>
|
|
1413
|
+
<AppRoutes />
|
|
1414
|
+
</Suspense>
|
|
1415
|
+
</FateClient>
|
|
1416
|
+
</template>
|
|
1270
1417
|
```
|
|
1271
1418
|
|
|
1272
|
-
|
|
1419
|
+
The `client` prop accepts a plain client, a ref, a computed value, or a getter. Descendants always read the current client, so switching credentials, endpoints, or transports does not require remounting the provider.
|
|
1273
1420
|
|
|
1274
|
-
|
|
1421
|
+
You can also install the client as a Vue plugin:
|
|
1275
1422
|
|
|
1276
|
-
```
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
});
|
|
1423
|
+
```ts
|
|
1424
|
+
import { createApp } from 'vue';
|
|
1425
|
+
import { createFatePlugin } from 'vue-fate';
|
|
1426
|
+
import { createFateClient } from 'vue-fate/client';
|
|
1427
|
+
import App from './App.vue';
|
|
1428
|
+
|
|
1429
|
+
const fate = createFateClient({ url: '/fate' });
|
|
1430
|
+
|
|
1431
|
+
createApp(App).use(createFatePlugin(fate)).mount('#app');
|
|
1283
1432
|
```
|
|
1284
1433
|
|
|
1285
|
-
###
|
|
1434
|
+
### Defining Views
|
|
1286
1435
|
|
|
1287
|
-
|
|
1436
|
+
Views are plain TypeScript values and can live anywhere. In Vue apps, it is usually best to define shared views in `.ts` modules and import them from single-file components:
|
|
1288
1437
|
|
|
1289
|
-
```
|
|
1290
|
-
import {
|
|
1438
|
+
```ts
|
|
1439
|
+
import type { Post, User } from '@your-org/server/views';
|
|
1440
|
+
import { view } from 'vue-fate';
|
|
1291
1441
|
|
|
1292
|
-
export const
|
|
1293
|
-
content: true,
|
|
1442
|
+
export const UserView = view<User>()({
|
|
1294
1443
|
id: true,
|
|
1444
|
+
name: true,
|
|
1445
|
+
username: true,
|
|
1295
1446
|
});
|
|
1296
1447
|
|
|
1297
|
-
export const
|
|
1298
|
-
author:
|
|
1299
|
-
|
|
1448
|
+
export const PostView = view<Post>()({
|
|
1449
|
+
author: UserView,
|
|
1450
|
+
id: true,
|
|
1451
|
+
title: true,
|
|
1300
1452
|
});
|
|
1301
1453
|
```
|
|
1302
1454
|
|
|
1303
|
-
|
|
1455
|
+
Vue can import values across components, but single-file components have one default component export. Keeping reusable views in `.ts` files avoids coupling your data model to component files and makes view composition straightforward.
|
|
1304
1456
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1457
|
+
### Requests
|
|
1458
|
+
|
|
1459
|
+
`useRequest` declares the data a route, page, or component tree needs. It returns a resource with `data`, `pending`, `error`, `ready`, `refresh`, and `dispose`:
|
|
1460
|
+
|
|
1461
|
+
```vue
|
|
1462
|
+
<script setup lang="ts">
|
|
1463
|
+
import { useListView, useRequest } from 'vue-fate';
|
|
1464
|
+
import { PostCardView } from '../fateViews';
|
|
1465
|
+
import PostCard from '../ui/PostCard.vue';
|
|
1466
|
+
|
|
1467
|
+
const request = useRequest({
|
|
1468
|
+
posts: {
|
|
1469
|
+
args: { first: 20 },
|
|
1470
|
+
list: PostCardView,
|
|
1311
1471
|
},
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
const { posts } = await request.ready();
|
|
1475
|
+
const [postItems, loadNext] = useListView(PostCardView, posts);
|
|
1476
|
+
</script>
|
|
1477
|
+
|
|
1478
|
+
<template>
|
|
1479
|
+
<PostCard v-for="{ node } in postItems" :key="node.id" :post="node" />
|
|
1480
|
+
<button v-if="loadNext" @click="loadNext()">Load more</button>
|
|
1481
|
+
</template>
|
|
1316
1482
|
```
|
|
1317
1483
|
|
|
1318
|
-
|
|
1484
|
+
Awaiting `ready()` in `<script setup>` participates in Vue Suspense. If you do not await it, read `request.data.value`, `request.pending.value`, and `request.error.value` in script, or use the refs directly in templates.
|
|
1319
1485
|
|
|
1320
|
-
|
|
1486
|
+
### Views in Components
|
|
1321
1487
|
|
|
1322
|
-
|
|
1488
|
+
Use `useView` to read a `ViewRef` from the normalized cache and subscribe to updates for the selected fields:
|
|
1323
1489
|
|
|
1324
|
-
```
|
|
1325
|
-
|
|
1490
|
+
```vue
|
|
1491
|
+
<script setup lang="ts">
|
|
1492
|
+
import type { ViewRef } from 'vue-fate';
|
|
1493
|
+
import { useView } from 'vue-fate';
|
|
1494
|
+
import { PostCardView, UserView } from '../fateViews';
|
|
1495
|
+
import UserCard from './UserCard.vue';
|
|
1326
1496
|
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
commentSearch: { args: { query }, list: commentView },
|
|
1331
|
-
events: { list: eventView },
|
|
1332
|
-
posts: { list: postView },
|
|
1333
|
-
viewer: { view: userView },
|
|
1497
|
+
const props = defineProps<{
|
|
1498
|
+
post: ViewRef<'Post'>;
|
|
1499
|
+
}>();
|
|
1334
1500
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1501
|
+
const post = useView(PostCardView, () => props.post);
|
|
1502
|
+
const author = useView(UserView, () => post.value?.author ?? null);
|
|
1503
|
+
</script>
|
|
1504
|
+
|
|
1505
|
+
<template>
|
|
1506
|
+
<article v-if="post">
|
|
1507
|
+
<h2>{{ post.title }}</h2>
|
|
1508
|
+
<UserCard v-if="author" :user="author" />
|
|
1509
|
+
</article>
|
|
1510
|
+
</template>
|
|
1339
1511
|
```
|
|
1340
1512
|
|
|
1341
|
-
|
|
1513
|
+
Pass reactive props through a getter so fate tracks prop changes. In script, resources are refs and need `.value`. In templates, Vue unwraps them automatically.
|
|
1342
1514
|
|
|
1343
|
-
|
|
1515
|
+
### Lists and Live Views
|
|
1344
1516
|
|
|
1345
|
-
|
|
1346
|
-
import { createFateServer, createHonoFateHandler } from '@nkzw/fate/server';
|
|
1347
|
-
import { createPrismaSourceAdapter } from '@nkzw/fate/server/prisma';
|
|
1348
|
-
import { Hono } from 'hono';
|
|
1349
|
-
import type { AppContext } from './context.ts';
|
|
1350
|
-
import { prisma } from './prisma.ts';
|
|
1351
|
-
import { Root, userDataView } from './views.ts';
|
|
1517
|
+
`useListView` subscribes to a connection returned from `useRequest` or from a nested view field:
|
|
1352
1518
|
|
|
1353
|
-
|
|
1519
|
+
```ts
|
|
1520
|
+
const [comments, loadNextCommentPage] = useListView(CommentView, () => post.value?.comments);
|
|
1521
|
+
```
|
|
1354
1522
|
|
|
1355
|
-
|
|
1356
|
-
prisma: (ctx) => ctx.prisma,
|
|
1357
|
-
views: Root,
|
|
1358
|
-
});
|
|
1523
|
+
`useLiveView` and `useLiveListView` have the same resource shape as `useView` and `useListView`, but they also subscribe to server-pushed updates when the selected transport supports live views:
|
|
1359
1524
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
sessionUser: await getSessionUser(adapterContext.req.raw),
|
|
1365
|
-
}),
|
|
1366
|
-
queries: {
|
|
1367
|
-
viewer: {
|
|
1368
|
-
resolve: ({ ctx, select }) =>
|
|
1369
|
-
sources.resolveById({
|
|
1370
|
-
ctx,
|
|
1371
|
-
id: ctx.sessionUser.id,
|
|
1372
|
-
input: { select },
|
|
1373
|
-
view: userDataView,
|
|
1374
|
-
}),
|
|
1375
|
-
},
|
|
1376
|
-
},
|
|
1377
|
-
roots: Root,
|
|
1378
|
-
sources,
|
|
1379
|
-
});
|
|
1525
|
+
```ts
|
|
1526
|
+
const post = useLiveView(PostCardView, () => props.post);
|
|
1527
|
+
const [comments] = useLiveListView(CommentView, () => post.value?.comments);
|
|
1528
|
+
```
|
|
1380
1529
|
|
|
1381
|
-
|
|
1382
|
-
const handler = createHonoFateHandler(fate);
|
|
1530
|
+
Manual cleanup works through `dispose()`:
|
|
1383
1531
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1532
|
+
```ts
|
|
1533
|
+
const post = useLiveView(PostCardView, () => props.post);
|
|
1534
|
+
|
|
1535
|
+
onBeforeUnmount(() => {
|
|
1536
|
+
post.dispose();
|
|
1537
|
+
});
|
|
1386
1538
|
```
|
|
1387
1539
|
|
|
1388
|
-
|
|
1540
|
+
Vue scope disposal also cleans up resources automatically.
|
|
1389
1541
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1542
|
+
### Mutations
|
|
1543
|
+
|
|
1544
|
+
Use `useFateClient` to access generated mutations:
|
|
1545
|
+
|
|
1546
|
+
```vue
|
|
1547
|
+
<script setup lang="ts">
|
|
1548
|
+
import { ref } from 'vue';
|
|
1549
|
+
import { useFateClient } from 'vue-fate';
|
|
1550
|
+
|
|
1551
|
+
const props = defineProps<{
|
|
1552
|
+
post: { id: string; likes: number };
|
|
1553
|
+
}>();
|
|
1554
|
+
|
|
1555
|
+
const fate = useFateClient();
|
|
1556
|
+
const pending = ref(false);
|
|
1557
|
+
const error = ref<unknown>(null);
|
|
1558
|
+
|
|
1559
|
+
const like = async () => {
|
|
1560
|
+
pending.value = true;
|
|
1561
|
+
error.value = null;
|
|
1562
|
+
|
|
1563
|
+
try {
|
|
1564
|
+
await fate.mutations.post.like({
|
|
1565
|
+
input: { id: props.post.id },
|
|
1566
|
+
optimistic: { likes: props.post.likes + 1 },
|
|
1567
|
+
});
|
|
1568
|
+
} catch (caughtError) {
|
|
1569
|
+
error.value = caughtError;
|
|
1570
|
+
} finally {
|
|
1571
|
+
pending.value = false;
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
</script>
|
|
1575
|
+
|
|
1576
|
+
<template>
|
|
1577
|
+
<button :disabled="pending" @click="like">Like</button>
|
|
1578
|
+
</template>
|
|
1579
|
+
```
|
|
1580
|
+
|
|
1581
|
+
The mutation call shape is the same as `react-fate`: `input`, `optimistic`, `insert`, and `view` work the same way. Vue does not have React Actions or `useActionState`, so Vue components should model pending and error state with Vue refs or your application state library.
|
|
1582
|
+
|
|
1583
|
+
### API Differences from React
|
|
1584
|
+
|
|
1585
|
+
The names intentionally mirror `react-fate` where Vue has an equivalent API. The main differences are Vue framework differences:
|
|
1586
|
+
|
|
1587
|
+
- `useRequest`, `useView`, and list hooks return Vue resources instead of throwing promises from render.
|
|
1588
|
+
- Async setup and `<Suspense>` replace React's async component model.
|
|
1589
|
+
- Mutations use `fate.mutations` directly; React-only `fate.actions` and `useActionState` patterns do not apply.
|
|
1590
|
+
- Shared views are best kept in `.ts` modules instead of exporting named views from component files.
|
|
1591
|
+
|
|
1592
|
+
The generated client, server integrations, cache behavior, masking, pagination, optimistic updates, and live transport behavior are shared across adapters.
|
|
1593
|
+
|
|
1594
|
+
## GraphQL Integration
|
|
1595
|
+
|
|
1596
|
+
_fate_ can use an existing GraphQL API as its transport. This keeps the adapter APIs, view composition, normalized cache, masking, requests, list views, live views, and mutations the same while replacing the native or tRPC backend with GraphQL operations.
|
|
1597
|
+
|
|
1598
|
+
Use the GraphQL transport when your backend already exposes GraphQL and you want fate's client model without adding fate's native server protocol.
|
|
1599
|
+
|
|
1600
|
+
### Template
|
|
1601
|
+
|
|
1602
|
+
Create a client for an existing GraphQL server with:
|
|
1603
|
+
|
|
1604
|
+
```bash
|
|
1605
|
+
vp create fate my-app --template graphql-client
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
Create a full GraphQL + Prisma example app with:
|
|
1609
|
+
|
|
1610
|
+
```bash
|
|
1611
|
+
vp create fate my-app --template graphql
|
|
1612
|
+
```
|
|
1613
|
+
|
|
1614
|
+
The client-only template is the smallest reference for the integration. It contains a `src/fate/graphql.ts` file that maps your GraphQL schema to fate views and roots.
|
|
1615
|
+
|
|
1616
|
+
### GraphQL Schema Shape
|
|
1617
|
+
|
|
1618
|
+
The GraphQL transport expects a schema with Relay-style object identity and pagination:
|
|
1619
|
+
|
|
1620
|
+
- Entity objects include `id` and `__typename`.
|
|
1621
|
+
- Object fetches go through a `nodes(ids:)` field.
|
|
1622
|
+
- List fields return Relay connections with `edges`, `cursor`, `node`, and `pageInfo`.
|
|
1623
|
+
- Root queries and mutations return the entity type selected by the fate view.
|
|
1624
|
+
|
|
1625
|
+
For example, a `Post` list can be exposed as a normal GraphQL connection:
|
|
1626
|
+
|
|
1627
|
+
```graphql
|
|
1628
|
+
type Query {
|
|
1629
|
+
posts(first: Int, after: String): PostConnection!
|
|
1630
|
+
viewer: User
|
|
1631
|
+
nodes(ids: [ID!]!): [Node]!
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
type PostConnection {
|
|
1635
|
+
edges: [PostEdge!]!
|
|
1636
|
+
pageInfo: PageInfo!
|
|
1637
|
+
}
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
If your schema uses different root field names, keep the fate names you want on the client and map them with `fateGraphQL.roots`.
|
|
1641
|
+
|
|
1642
|
+
### Mapping Your Schema
|
|
1643
|
+
|
|
1644
|
+
Create a module that exports data views, `Root`, and an optional `fateGraphQL` config. The Vite plugin reads this module during development and build time, generates the client wiring, and leaves your runtime GraphQL server unchanged.
|
|
1645
|
+
|
|
1646
|
+
```tsx
|
|
1647
|
+
import { graphqlMutation } from '@nkzw/fate';
|
|
1648
|
+
import { dataView, list, type Entity } from '@nkzw/fate/server';
|
|
1649
|
+
|
|
1650
|
+
type GraphQLUser = {
|
|
1651
|
+
id: string;
|
|
1652
|
+
name?: string | null;
|
|
1653
|
+
username?: string | null;
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
type GraphQLPost = {
|
|
1657
|
+
author?: GraphQLUser | null;
|
|
1658
|
+
id: string;
|
|
1659
|
+
title: string;
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
export const userDataView = dataView<GraphQLUser>('User')({
|
|
1663
|
+
id: true,
|
|
1664
|
+
name: true,
|
|
1665
|
+
username: true,
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
export const postDataView = dataView<GraphQLPost>('Post')({
|
|
1669
|
+
author: userDataView,
|
|
1670
|
+
id: true,
|
|
1671
|
+
title: true,
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
export type User = Entity<typeof userDataView, 'User'>;
|
|
1675
|
+
export type Post = Entity<
|
|
1676
|
+
typeof postDataView,
|
|
1677
|
+
'Post',
|
|
1678
|
+
{
|
|
1679
|
+
author: User | null;
|
|
1680
|
+
}
|
|
1681
|
+
>;
|
|
1682
|
+
|
|
1683
|
+
export const Root = {
|
|
1684
|
+
posts: list(postDataView),
|
|
1685
|
+
viewer: userDataView,
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
export const fateGraphQL = {
|
|
1689
|
+
roots: {
|
|
1690
|
+
posts: { field: 'posts' },
|
|
1691
|
+
viewer: { field: 'viewer' },
|
|
1692
|
+
},
|
|
1693
|
+
} as const;
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
The data views describe the fields client components are allowed to select. `Root` describes the root operations available to `useRequest`. `fateGraphQL.roots` maps those root names to actual GraphQL fields. If the GraphQL field has the same name as the fate root, the `field` entry can be omitted.
|
|
1697
|
+
|
|
1698
|
+
### Vite Plugin
|
|
1699
|
+
|
|
1700
|
+
Configure the fate Vite plugin with the GraphQL transport and point it at the mapping module:
|
|
1701
|
+
|
|
1702
|
+
::: code-group
|
|
1703
|
+
|
|
1704
|
+
```tsx [React]
|
|
1705
|
+
import { fate } from 'react-fate/vite';
|
|
1706
|
+
import { defineConfig } from 'vite';
|
|
1707
|
+
|
|
1708
|
+
export default defineConfig({
|
|
1709
|
+
plugins: [
|
|
1710
|
+
fate({
|
|
1711
|
+
module: './src/fate/graphql.ts',
|
|
1712
|
+
transport: 'graphql',
|
|
1713
|
+
}),
|
|
1714
|
+
],
|
|
1715
|
+
});
|
|
1716
|
+
```
|
|
1717
|
+
|
|
1718
|
+
```ts [Vue]
|
|
1719
|
+
import vue from '@vitejs/plugin-vue';
|
|
1720
|
+
import { fate } from 'vue-fate/vite';
|
|
1721
|
+
import { defineConfig } from 'vite';
|
|
1722
|
+
|
|
1723
|
+
export default defineConfig({
|
|
1724
|
+
plugins: [
|
|
1725
|
+
vue(),
|
|
1726
|
+
fate({
|
|
1727
|
+
module: './src/fate/graphql.ts',
|
|
1728
|
+
transport: 'graphql',
|
|
1729
|
+
}),
|
|
1730
|
+
],
|
|
1731
|
+
});
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
:::
|
|
1735
|
+
|
|
1736
|
+
The plugin generates a typed `createFateClient` helper from your views, roots, and GraphQL mapping. It also watches the mapping module and the files it imports during development.
|
|
1737
|
+
|
|
1738
|
+
### Creating a Client
|
|
1739
|
+
|
|
1740
|
+
Create the client with your GraphQL endpoint and provide it through the `FateClient` provider:
|
|
1741
|
+
|
|
1742
|
+
::: code-group
|
|
1743
|
+
|
|
1744
|
+
```tsx [React]
|
|
1745
|
+
import { FateClient } from 'react-fate';
|
|
1746
|
+
import { createFateClient } from 'react-fate/client';
|
|
1747
|
+
|
|
1748
|
+
const fate = createFateClient({
|
|
1749
|
+
headers: () => ({
|
|
1750
|
+
authorization: `Bearer ${token}`,
|
|
1751
|
+
}),
|
|
1752
|
+
url: 'https://api.example.com/graphql',
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
export function App() {
|
|
1756
|
+
return <FateClient client={fate}>{/* Components go here */}</FateClient>;
|
|
1757
|
+
}
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
```vue [Vue]
|
|
1761
|
+
<script setup lang="ts">
|
|
1762
|
+
import { FateClient } from 'vue-fate';
|
|
1763
|
+
import { createFateClient } from 'vue-fate/client';
|
|
1764
|
+
import AppRoutes from './AppRoutes.vue';
|
|
1765
|
+
|
|
1766
|
+
const fate = createFateClient({
|
|
1767
|
+
headers: () => ({
|
|
1768
|
+
authorization: `Bearer ${token}`,
|
|
1769
|
+
}),
|
|
1770
|
+
url: 'https://api.example.com/graphql',
|
|
1771
|
+
});
|
|
1772
|
+
</script>
|
|
1773
|
+
|
|
1774
|
+
<template>
|
|
1775
|
+
<FateClient :client="fate">
|
|
1776
|
+
<AppRoutes />
|
|
1777
|
+
</FateClient>
|
|
1778
|
+
</template>
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
:::
|
|
1782
|
+
|
|
1783
|
+
Use `fetch` when you need to customize credentials or reuse an application fetch wrapper:
|
|
1784
|
+
|
|
1785
|
+
```tsx
|
|
1786
|
+
const fate = createFateClient({
|
|
1787
|
+
fetch: (input, init) =>
|
|
1788
|
+
fetch(input, {
|
|
1789
|
+
...init,
|
|
1790
|
+
credentials: 'include',
|
|
1791
|
+
}),
|
|
1792
|
+
url: `${env('SERVER_URL')}/graphql`,
|
|
1793
|
+
});
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
GraphQL operations issued in the same microtask are batched into a single GraphQL query or mutation document with aliased fields.
|
|
1797
|
+
|
|
1798
|
+
Deferred view fields work with the GraphQL transport through the same normalized cache flow as native HTTP: the eager query omits `defer(...)` fields, and `useView`, `useListView`, or `useLiveListView` fetches the missing selection through `nodes(ids:)` when the deferred handle is read. GraphQL `@defer` is the natural wire format for this feature, but fate's GraphQL transport currently expects one JSON result per operation and does not consume incremental multipart patches yet.
|
|
1799
|
+
|
|
1800
|
+
### Object IDs
|
|
1801
|
+
|
|
1802
|
+
The transport converts between fate entity IDs and GraphQL node IDs. By default, it sends IDs as `${type}-${id}` and strips that prefix from returned IDs. Override this if your schema uses Relay global IDs, raw database IDs, or another encoding:
|
|
1803
|
+
|
|
1804
|
+
```tsx
|
|
1805
|
+
const fate = createFateClient({
|
|
1806
|
+
decodeNodeId: (type, id) => {
|
|
1807
|
+
const [nodeType, nodeId] = atob(String(id)).split(':');
|
|
1808
|
+
if (nodeType !== type) {
|
|
1809
|
+
throw new Error(`Expected a ${type} node id.`);
|
|
1810
|
+
}
|
|
1811
|
+
return nodeId;
|
|
1812
|
+
},
|
|
1813
|
+
encodeNodeId: (type, id) => btoa(`${type}:${id}`),
|
|
1814
|
+
url: '/graphql',
|
|
1815
|
+
});
|
|
1816
|
+
```
|
|
1817
|
+
|
|
1818
|
+
If your GraphQL API already accepts and returns the same IDs you use in the app, return `id` from both functions.
|
|
1819
|
+
|
|
1820
|
+
### Requests and Arguments
|
|
1821
|
+
|
|
1822
|
+
Client code keeps using `useRequest` with the same shape as the other transports:
|
|
1823
|
+
|
|
1824
|
+
```tsx
|
|
1825
|
+
const { posts, viewer } = useRequest({
|
|
1826
|
+
posts: {
|
|
1827
|
+
args: { first: 10 },
|
|
1828
|
+
list: PostView,
|
|
1829
|
+
},
|
|
1830
|
+
viewer: { view: UserView },
|
|
1831
|
+
});
|
|
1832
|
+
```
|
|
1833
|
+
|
|
1834
|
+
Root arguments are sent to the root GraphQL field. Nested relation arguments are scoped by relation name:
|
|
1835
|
+
|
|
1836
|
+
```tsx
|
|
1837
|
+
const { posts } = useRequest({
|
|
1838
|
+
posts: {
|
|
1839
|
+
args: {
|
|
1840
|
+
comments: { first: 3 },
|
|
1841
|
+
first: 10,
|
|
1842
|
+
},
|
|
1843
|
+
list: PostWithCommentsView,
|
|
1844
|
+
},
|
|
1845
|
+
});
|
|
1846
|
+
```
|
|
1847
|
+
|
|
1848
|
+
This produces a root `posts(first: 10)` field and a nested `comments(first: 3)` field in the generated GraphQL selection.
|
|
1849
|
+
|
|
1850
|
+
### Mutations
|
|
1851
|
+
|
|
1852
|
+
Map fate mutation names to GraphQL mutation fields with `graphqlMutation`:
|
|
1853
|
+
|
|
1854
|
+
```tsx
|
|
1855
|
+
export const fateGraphQL = {
|
|
1856
|
+
mutations: {
|
|
1857
|
+
'post.like': graphqlMutation<Post, { id: string }, Post>('Post', {
|
|
1858
|
+
field: 'postLike',
|
|
1859
|
+
}),
|
|
1860
|
+
},
|
|
1861
|
+
roots: {
|
|
1862
|
+
posts: { field: 'posts' },
|
|
1863
|
+
},
|
|
1864
|
+
} as const;
|
|
1865
|
+
```
|
|
1866
|
+
|
|
1867
|
+
By default, the input is sent as an `input` argument:
|
|
1868
|
+
|
|
1869
|
+
```graphql
|
|
1870
|
+
mutation {
|
|
1871
|
+
postLike(input: { id: "12" }) {
|
|
1872
|
+
id
|
|
1873
|
+
likes
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
Use `inputArg` when your schema uses a different argument name, or `inputArg: false` when the input object should be spread into field arguments:
|
|
1879
|
+
|
|
1880
|
+
```tsx
|
|
1881
|
+
export const fateGraphQL = {
|
|
1882
|
+
mutations: {
|
|
1883
|
+
'post.like': graphqlMutation<Post, { id: string }, Post>('Post', {
|
|
1884
|
+
field: 'likePost',
|
|
1885
|
+
inputArg: 'payload',
|
|
1886
|
+
}),
|
|
1887
|
+
'user.follow': graphqlMutation<User, { id: string }, User>('User', {
|
|
1888
|
+
field: 'followUser',
|
|
1889
|
+
inputArg: false,
|
|
1890
|
+
}),
|
|
1891
|
+
},
|
|
1892
|
+
} as const;
|
|
1893
|
+
```
|
|
1894
|
+
|
|
1895
|
+
Mutations use the same `mutation(...)` API described in the [Actions Guide](/docs/guide/actions.md). React clients can also expose those mutations as Actions for `useActionState`.
|
|
1896
|
+
|
|
1897
|
+
### Live Views
|
|
1898
|
+
|
|
1899
|
+
GraphQL live views use [GraphQL SSE](https://github.com/enisdenjo/graphql-sse). Install `graphql-sse` in the client package and leave `live` enabled, or pass `live: false` when your schema does not support subscriptions.
|
|
1900
|
+
|
|
1901
|
+
```tsx
|
|
1902
|
+
const fate = createFateClient({
|
|
1903
|
+
live: {
|
|
1904
|
+
url: 'https://api.example.com/graphql/stream',
|
|
1905
|
+
},
|
|
1906
|
+
url: 'https://api.example.com/graphql',
|
|
1907
|
+
});
|
|
1908
|
+
```
|
|
1909
|
+
|
|
1910
|
+
The default subscription fields are `fateLiveNode` for `useLiveView` and `fateLiveConnection` for `useLiveListView`. Rename them with `entityField` and `connectionField`:
|
|
1911
|
+
|
|
1912
|
+
```tsx
|
|
1913
|
+
const fate = createFateClient({
|
|
1914
|
+
live: {
|
|
1915
|
+
connectionField: 'liveConnection',
|
|
1916
|
+
entityField: 'liveNode',
|
|
1917
|
+
url: '/graphql/stream',
|
|
1918
|
+
},
|
|
1919
|
+
url: '/graphql',
|
|
1920
|
+
});
|
|
1921
|
+
```
|
|
1922
|
+
|
|
1923
|
+
The live node subscription returns `{ data, delete, id, select }`. The live connection subscription returns events such as `appendNode`, `prependNode`, `deleteEdge`, and `invalidate`. These payloads match fate's live transport events, so the cache update behavior is the same as the native transport.
|
|
1924
|
+
|
|
1925
|
+
If you do not need live views, disable them explicitly:
|
|
1926
|
+
|
|
1927
|
+
```tsx
|
|
1928
|
+
const fate = createFateClient({
|
|
1929
|
+
live: false,
|
|
1930
|
+
url: '/graphql',
|
|
1931
|
+
});
|
|
1932
|
+
```
|
|
1933
|
+
|
|
1934
|
+
### Existing Servers
|
|
1935
|
+
|
|
1936
|
+
The GraphQL transport is intentionally a mapping layer. It does not require `createFateServer`, the Prisma adapter, or the Drizzle adapter. Your GraphQL server remains responsible for authorization, validation, resolver behavior, cursor pagination, and mutation side effects.
|
|
1937
|
+
|
|
1938
|
+
Use data views to expose only the fields the client should be able to select, keep GraphQL schema authorization in your server, and treat `src/fate/graphql.ts` as the contract between your GraphQL API and fate's client.
|
|
1939
|
+
|
|
1940
|
+
## Server Integration
|
|
1941
|
+
|
|
1942
|
+
Until now, we have focused on the client-side API of fate. You'll need a backend that can be wired into fate's typed request model so the Vite plugin can connect the typed fate APIs to your app. _fate_ currently ships three integration paths:
|
|
1943
|
+
|
|
1944
|
+
- The native fate protocol, which is transport-agnostic and can be hosted by any Fetch-compatible server.
|
|
1945
|
+
- The tRPC adapter, which keeps compatibility with existing tRPC backends.
|
|
1946
|
+
- The [GraphQL transport](/docs/integrations/graphql.md), which maps fate views and roots to an existing GraphQL schema.
|
|
1947
|
+
|
|
1948
|
+
_fate_ currently provides database adapters for Prisma and Drizzle, but the framework itself is not coupled to a particular ORM. The adapters plug into the same source execution runtime and can be exposed through the native protocol or through tRPC.
|
|
1949
|
+
|
|
1950
|
+
### Conventions & Object Identity
|
|
1951
|
+
|
|
1952
|
+
fate expects that data is served by a backend that follows these conventions:
|
|
1953
|
+
|
|
1954
|
+
- A `byId` query for each data type to fetch individual objects by their unique identifier (`id`).
|
|
1955
|
+
- A `list` query for fetching lists of objects with support for pagination.
|
|
1956
|
+
|
|
1957
|
+
Objects are identified by their ID and type name (`__typename`, e.g. `Post`, `User`), and stored by `__typename:id` (e.g. "Post:123") in the client cache. fate keeps list orderings under stable keys derived from the backend procedure and args. Relations are stored as IDs and returned to components as ViewRef tokens.
|
|
1958
|
+
|
|
1959
|
+
fate's type definitions might seem verbose at first glance. However, with fate's minimal API surface, AI tools can easily write this code for you, and the Vite plugin takes care of connecting it to your app.
|
|
1960
|
+
|
|
1961
|
+
> [!NOTE]
|
|
1962
|
+
> You can adopt _fate_ incrementally in an existing tRPC codebase without changing your existing schema by adding these queries alongside your existing procedures.
|
|
1963
|
+
|
|
1964
|
+
### Data Views
|
|
1965
|
+
|
|
1966
|
+
Since clients can send arbitrary selection objects to the server, we need to implement a way to translate these selection objects into database queries without exposing raw database queries and private data to the client. On the client, we define views to select fields on each type. We can do the same on the server using fate data views and the `dataView` function from `@nkzw/fate/server`.
|
|
1967
|
+
|
|
1968
|
+
Create a `views.ts` file next to your server entry that exports the data views for each type. The same data view shape works with both Prisma model types and Drizzle row types:
|
|
1969
|
+
|
|
1970
|
+
::: code-group
|
|
1971
|
+
|
|
1972
|
+
```tsx [Prisma]
|
|
1973
|
+
import { dataView, type Entity } from '@nkzw/fate/server';
|
|
1974
|
+
import type { User as PrismaUser } from '../prisma/prisma-client/client.ts';
|
|
1975
|
+
|
|
1976
|
+
export const userDataView = dataView<PrismaUser>('User')({
|
|
1977
|
+
id: true,
|
|
1978
|
+
name: true,
|
|
1979
|
+
username: true,
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
export type User = Entity<typeof userDataView, 'User'>;
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
```tsx [Drizzle]
|
|
1986
|
+
import { dataView, type Entity } from '@nkzw/fate/server';
|
|
1987
|
+
import type { UserRow } from '../drizzle/schema.ts';
|
|
1988
|
+
|
|
1989
|
+
export const userDataView = dataView<UserRow>('User')({
|
|
1990
|
+
id: true,
|
|
1991
|
+
name: true,
|
|
1992
|
+
username: true,
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
export type User = Entity<typeof userDataView, 'User'>;
|
|
1996
|
+
```
|
|
1997
|
+
|
|
1998
|
+
:::
|
|
1999
|
+
|
|
2000
|
+
Now that we apply `userDataView` to the `byId` query, the server limits the selection to the fields defined in the data view, keeping private fields hidden from the client, and providing type safety for client views:
|
|
2001
|
+
|
|
2002
|
+
```tsx
|
|
2003
|
+
const UserData = view<User>()({
|
|
2004
|
+
// Type-error + ignored during runtime.
|
|
2005
|
+
password: true,
|
|
2006
|
+
});
|
|
2007
|
+
```
|
|
2008
|
+
|
|
2009
|
+
### Data View Composition
|
|
2010
|
+
|
|
2011
|
+
Similar to client-side views, data views can be composed of other data views:
|
|
2012
|
+
|
|
2013
|
+
```tsx
|
|
2014
|
+
export const postDataView = dataView<PostItem>('Post')({
|
|
2015
|
+
author: userDataView,
|
|
2016
|
+
content: true,
|
|
2017
|
+
id: true,
|
|
2018
|
+
title: true,
|
|
2019
|
+
});
|
|
2020
|
+
```
|
|
2021
|
+
|
|
2022
|
+
### Data View Lists
|
|
2023
|
+
|
|
2024
|
+
Use the `list` helper to define list fields:
|
|
2025
|
+
|
|
2026
|
+
```tsx
|
|
2027
|
+
import { list } from '@nkzw/fate/server';
|
|
2028
|
+
|
|
2029
|
+
export const commentDataView = dataView<CommentItem>('Comment')({
|
|
2030
|
+
content: true,
|
|
2031
|
+
id: true,
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
export const postDataView = dataView<PostItem>('Post')({
|
|
2035
|
+
author: userDataView,
|
|
2036
|
+
comments: list(commentDataView, { orderBy: [{ createdAt: 'asc' }, { id: 'asc' }] }),
|
|
2037
|
+
});
|
|
2038
|
+
```
|
|
2039
|
+
|
|
2040
|
+
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:
|
|
2041
|
+
|
|
2042
|
+
```tsx
|
|
2043
|
+
export const Root = {
|
|
2044
|
+
categories: list(categoryDataView, { orderBy: [{ createdAt: 'asc' }, { id: 'asc' }] }),
|
|
2045
|
+
commentSearch: {
|
|
2046
|
+
procedure: 'search',
|
|
2047
|
+
view: list(commentDataView, { orderBy: [{ createdAt: 'desc' }, { id: 'desc' }] }),
|
|
2048
|
+
},
|
|
2049
|
+
events: list(eventDataView, { orderBy: [{ startAt: 'asc' }, { id: 'asc' }] }),
|
|
2050
|
+
posts: list(postDataView, { orderBy: { createdAt: 'desc', id: 'desc' } }),
|
|
2051
|
+
viewer: userDataView,
|
|
2052
|
+
};
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
Entries that wrap their view in `list(...)` are treated as list resolvers. In the native protocol, the root key is the operation name used by the client. In the tRPC adapter, `procedure` can point that root at a specific router procedure. If you omit `list(...)`, fate treats the entry as a standard query.
|
|
2056
|
+
|
|
2057
|
+
You can pass default list options such as `orderBy` to `list(...)`. Ordering is scoped to that specific list wrapper: `Root.posts` can order posts by `createdAt desc`, while `categoryDataView.posts` or `postDataView.comments` can choose their own order. If no order is provided, fate orders by `id asc`. fate always appends `id asc` as a tie-breaker when no `id` order is present; include `id` yourself when you need a different tie-breaker direction such as `id desc`. Use the array form when ordering by multiple fields so the priority is unambiguous.
|
|
2058
|
+
|
|
2059
|
+
For the above `Root` definitions, you can make the following requests using `useRequest`:
|
|
2060
|
+
|
|
2061
|
+
```tsx
|
|
2062
|
+
const query = 'Apple';
|
|
2063
|
+
|
|
2064
|
+
const { posts, categories, viewer } = useRequest({
|
|
2065
|
+
// Explicit Root queries:
|
|
2066
|
+
categories: { list: categoryView },
|
|
2067
|
+
commentSearch: { args: { query }, list: commentView },
|
|
2068
|
+
events: { list: eventView },
|
|
2069
|
+
posts: { list: postView },
|
|
2070
|
+
viewer: { view: userView },
|
|
2071
|
+
|
|
2072
|
+
// Queries by id, if those entities have a `byId` query defined:
|
|
2073
|
+
post: { id: '12', view: postView },
|
|
2074
|
+
comment: { ids: ['6', '7'], view: commentView },
|
|
2075
|
+
});
|
|
2076
|
+
```
|
|
2077
|
+
|
|
2078
|
+
### Native fate protocol
|
|
2079
|
+
|
|
2080
|
+
The native protocol keeps tRPC optional. Create a source adapter from your ORM integration, pass it to `createFateServer`, and expose the returned server through a Fetch-compatible handler.
|
|
2081
|
+
|
|
2082
|
+
```tsx
|
|
2083
|
+
import { createFateServer, createHonoFateHandler } from '@nkzw/fate/server';
|
|
2084
|
+
import { createPrismaSourceAdapter } from '@nkzw/fate/server/prisma';
|
|
2085
|
+
import { Hono } from 'hono';
|
|
2086
|
+
import type { AppContext } from './context.ts';
|
|
2087
|
+
import { prisma } from './prisma.ts';
|
|
2088
|
+
import { Root, userDataView } from './views.ts';
|
|
2089
|
+
|
|
2090
|
+
export { Root } from './views.ts';
|
|
2091
|
+
|
|
2092
|
+
const sources = createPrismaSourceAdapter<AppContext>({
|
|
2093
|
+
prisma: (ctx) => ctx.prisma,
|
|
2094
|
+
views: Root,
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
export const fate = createFateServer({
|
|
2098
|
+
context: async ({ adapterContext }) => ({
|
|
2099
|
+
prisma,
|
|
2100
|
+
request: adapterContext.req.raw,
|
|
2101
|
+
sessionUser: await getSessionUser(adapterContext.req.raw),
|
|
2102
|
+
}),
|
|
2103
|
+
queries: {
|
|
2104
|
+
viewer: {
|
|
2105
|
+
resolve: ({ ctx, select }) =>
|
|
2106
|
+
sources.resolveById({
|
|
2107
|
+
ctx,
|
|
2108
|
+
id: ctx.sessionUser.id,
|
|
2109
|
+
input: { select },
|
|
2110
|
+
view: userDataView,
|
|
2111
|
+
}),
|
|
2112
|
+
},
|
|
2113
|
+
},
|
|
2114
|
+
roots: Root,
|
|
2115
|
+
sources,
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
const app = new Hono();
|
|
2119
|
+
const handler = createHonoFateHandler(fate);
|
|
2120
|
+
|
|
2121
|
+
app.post('/fate', handler);
|
|
2122
|
+
app.post('/fate/live', handler);
|
|
2123
|
+
```
|
|
2124
|
+
|
|
2125
|
+
Configure the Vite plugin with the native transport:
|
|
2126
|
+
|
|
2127
|
+
::: code-group
|
|
2128
|
+
|
|
2129
|
+
```tsx [React]
|
|
2130
|
+
import { fate } from 'react-fate/vite';
|
|
2131
|
+
import { defineConfig } from 'vite';
|
|
2132
|
+
|
|
2133
|
+
export default defineConfig({
|
|
2134
|
+
plugins: [
|
|
2135
|
+
fate({
|
|
2136
|
+
module: '@your-org/server/fate.ts',
|
|
2137
|
+
transport: 'native',
|
|
2138
|
+
}),
|
|
2139
|
+
],
|
|
2140
|
+
});
|
|
2141
|
+
```
|
|
2142
|
+
|
|
2143
|
+
```ts [Vue]
|
|
2144
|
+
import vue from '@vitejs/plugin-vue';
|
|
2145
|
+
import { fate } from 'vue-fate/vite';
|
|
2146
|
+
import { defineConfig } from 'vite';
|
|
1393
2147
|
|
|
1394
2148
|
export default defineConfig({
|
|
1395
2149
|
plugins: [
|
|
2150
|
+
vue(),
|
|
1396
2151
|
fate({
|
|
1397
2152
|
module: '@your-org/server/fate.ts',
|
|
1398
2153
|
transport: 'native',
|
|
@@ -1401,9 +2156,13 @@ export default defineConfig({
|
|
|
1401
2156
|
});
|
|
1402
2157
|
```
|
|
1403
2158
|
|
|
2159
|
+
:::
|
|
2160
|
+
|
|
1404
2161
|
With the native transport, the Vite plugin handles the HTTP transport setup. If you need to create a client manually, use `createFateClient` with the same route:
|
|
1405
2162
|
|
|
1406
|
-
|
|
2163
|
+
::: code-group
|
|
2164
|
+
|
|
2165
|
+
```tsx [React]
|
|
1407
2166
|
import { createFateClient } from 'react-fate/client';
|
|
1408
2167
|
|
|
1409
2168
|
const client = createFateClient({
|
|
@@ -1411,6 +2170,16 @@ const client = createFateClient({
|
|
|
1411
2170
|
});
|
|
1412
2171
|
```
|
|
1413
2172
|
|
|
2173
|
+
```ts [Vue]
|
|
2174
|
+
import { createFateClient } from 'vue-fate/client';
|
|
2175
|
+
|
|
2176
|
+
const client = createFateClient({
|
|
2177
|
+
url: '/fate',
|
|
2178
|
+
});
|
|
2179
|
+
```
|
|
2180
|
+
|
|
2181
|
+
:::
|
|
2182
|
+
|
|
1414
2183
|
The HTTP transport batches operations issued in the same microtask into one `POST /fate` request. Live views use one `GET /fate/live` SSE stream per fate client and `POST /fate/live` control messages when views subscribe or unsubscribe.
|
|
1415
2184
|
|
|
1416
2185
|
#### Custom Queries
|
|
@@ -1787,7 +2556,9 @@ export * from './views.ts';
|
|
|
1787
2556
|
|
|
1788
2557
|
Configure the fate Vite plugin with your server module:
|
|
1789
2558
|
|
|
1790
|
-
|
|
2559
|
+
::: code-group
|
|
2560
|
+
|
|
2561
|
+
```tsx [React]
|
|
1791
2562
|
import { fate } from 'react-fate/vite';
|
|
1792
2563
|
import { defineConfig } from 'vite';
|
|
1793
2564
|
|
|
@@ -1800,11 +2571,28 @@ export default defineConfig({
|
|
|
1800
2571
|
});
|
|
1801
2572
|
```
|
|
1802
2573
|
|
|
2574
|
+
```ts [Vue]
|
|
2575
|
+
import vue from '@vitejs/plugin-vue';
|
|
2576
|
+
import { fate } from 'vue-fate/vite';
|
|
2577
|
+
import { defineConfig } from 'vite';
|
|
2578
|
+
|
|
2579
|
+
export default defineConfig({
|
|
2580
|
+
plugins: [
|
|
2581
|
+
vue(),
|
|
2582
|
+
fate({
|
|
2583
|
+
module: '@your-org/server/trpc/router.ts',
|
|
2584
|
+
}),
|
|
2585
|
+
],
|
|
2586
|
+
});
|
|
2587
|
+
```
|
|
2588
|
+
|
|
2589
|
+
:::
|
|
2590
|
+
|
|
1803
2591
|
_Note: fate uses the specified server module name to find the server types it needs. Make sure that the module is available to the client package's Vite config._
|
|
1804
2592
|
|
|
1805
2593
|
During development, the plugin watches the server module and the files it imports. When one of those files changes, fate updates the internal client wiring and invalidates `@nkzw/fate/client` in Vite's module graph.
|
|
1806
2594
|
|
|
1807
|
-
For a barebones client without
|
|
2595
|
+
For a barebones client without a framework adapter, import the plugin from `@nkzw/fate/vite` and the client APIs from `@nkzw/fate/client`. The plugin wires the same server types for the selected import path.
|
|
1808
2596
|
|
|
1809
2597
|
The plugin writes project-local types under `.fate/`. If your TypeScript config does not already include dot-directories, extend the generated config:
|
|
1810
2598
|
|
|
@@ -1816,9 +2604,11 @@ The plugin writes project-local types under `.fate/`. If your TypeScript config
|
|
|
1816
2604
|
|
|
1817
2605
|
### Creating a _fate_ Client
|
|
1818
2606
|
|
|
1819
|
-
Now that the Vite plugin has connected the types, create a fate client instance and provide it to your
|
|
2607
|
+
Now that the Vite plugin has connected the types, create a fate client instance and provide it to your app with the `FateClient` provider:
|
|
1820
2608
|
|
|
1821
|
-
|
|
2609
|
+
::: code-group
|
|
2610
|
+
|
|
2611
|
+
```tsx [React]
|
|
1822
2612
|
import { httpBatchLink } from '@trpc/client';
|
|
1823
2613
|
import { FateClient } from 'react-fate';
|
|
1824
2614
|
import { createFateClient } from 'react-fate/client';
|
|
@@ -1844,8 +2634,291 @@ export function App() {
|
|
|
1844
2634
|
}
|
|
1845
2635
|
```
|
|
1846
2636
|
|
|
2637
|
+
```vue [Vue]
|
|
2638
|
+
<script setup lang="ts">
|
|
2639
|
+
import { httpBatchLink } from '@trpc/client';
|
|
2640
|
+
import { computed } from 'vue';
|
|
2641
|
+
import { FateClient } from 'vue-fate';
|
|
2642
|
+
import { createFateClient } from 'vue-fate/client';
|
|
2643
|
+
import AppRoutes from './AppRoutes.vue';
|
|
2644
|
+
|
|
2645
|
+
const fate = computed(() =>
|
|
2646
|
+
createFateClient({
|
|
2647
|
+
links: [
|
|
2648
|
+
httpBatchLink({
|
|
2649
|
+
fetch: (input, init) =>
|
|
2650
|
+
fetch(input, {
|
|
2651
|
+
...init,
|
|
2652
|
+
credentials: 'include',
|
|
2653
|
+
}),
|
|
2654
|
+
url: `${env('SERVER_URL')}/trpc`,
|
|
2655
|
+
}),
|
|
2656
|
+
],
|
|
2657
|
+
}),
|
|
2658
|
+
);
|
|
2659
|
+
</script>
|
|
2660
|
+
|
|
2661
|
+
<template>
|
|
2662
|
+
<FateClient :client="fate">
|
|
2663
|
+
<AppRoutes />
|
|
2664
|
+
</FateClient>
|
|
2665
|
+
</template>
|
|
2666
|
+
```
|
|
2667
|
+
|
|
2668
|
+
:::
|
|
2669
|
+
|
|
1847
2670
|
_And you are all set. Happy building!_
|
|
1848
2671
|
|
|
2672
|
+
## Void Integration
|
|
2673
|
+
|
|
2674
|
+
`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.
|
|
2675
|
+
|
|
2676
|
+
Use this integration when your app runs on Void and you want the example app's
|
|
2677
|
+
setup without copying its adapter glue.
|
|
2678
|
+
|
|
2679
|
+
### Install
|
|
2680
|
+
|
|
2681
|
+
::: code-group
|
|
2682
|
+
|
|
2683
|
+
```sh [React]
|
|
2684
|
+
pnpm add @nkzw/fate react-fate void-fate void @void/react
|
|
2685
|
+
```
|
|
2686
|
+
|
|
2687
|
+
```sh [Vue]
|
|
2688
|
+
pnpm add @nkzw/fate vue-fate void-fate void @void/vue
|
|
2689
|
+
```
|
|
2690
|
+
|
|
2691
|
+
:::
|
|
2692
|
+
|
|
2693
|
+
### Vite
|
|
2694
|
+
|
|
2695
|
+
Use the framework adapter's Vite plugin with the Void transport:
|
|
2696
|
+
|
|
2697
|
+
::: code-group
|
|
2698
|
+
|
|
2699
|
+
```tsx [React]
|
|
2700
|
+
import { voidReact } from '@void/react/plugin';
|
|
2701
|
+
import { fate } from 'react-fate/vite';
|
|
2702
|
+
import { defineConfig } from 'vite-plus';
|
|
2703
|
+
import { voidPlugin } from 'void';
|
|
2704
|
+
|
|
2705
|
+
export default defineConfig({
|
|
2706
|
+
plugins: [
|
|
2707
|
+
voidPlugin(),
|
|
2708
|
+
voidReact(),
|
|
2709
|
+
fate({
|
|
2710
|
+
module: './src/fate/server.ts',
|
|
2711
|
+
transport: 'void',
|
|
2712
|
+
}),
|
|
2713
|
+
],
|
|
2714
|
+
});
|
|
2715
|
+
```
|
|
2716
|
+
|
|
2717
|
+
```ts [Vue]
|
|
2718
|
+
import { voidVue } from '@void/vue/plugin';
|
|
2719
|
+
import { fate } from 'vue-fate/vite';
|
|
2720
|
+
import { defineConfig } from 'vite-plus';
|
|
2721
|
+
import { voidPlugin } from 'void';
|
|
2722
|
+
|
|
2723
|
+
export default defineConfig({
|
|
2724
|
+
plugins: [
|
|
2725
|
+
voidPlugin(),
|
|
2726
|
+
voidVue(),
|
|
2727
|
+
fate({
|
|
2728
|
+
module: './src/fate/server.ts',
|
|
2729
|
+
transport: 'void',
|
|
2730
|
+
}),
|
|
2731
|
+
],
|
|
2732
|
+
});
|
|
2733
|
+
```
|
|
2734
|
+
|
|
2735
|
+
:::
|
|
2736
|
+
|
|
2737
|
+
The Void transport uses `/fate` for RPC requests and `/fate-live` for live
|
|
2738
|
+
updates by default. In SSR, it calls the exported fate server directly. In the
|
|
2739
|
+
browser, it uses fetch and the SSE live endpoint.
|
|
2740
|
+
|
|
2741
|
+
### Server Setup
|
|
2742
|
+
|
|
2743
|
+
Create a Void live adapter with `createVoidFateLive`, pass its `live` event bus
|
|
2744
|
+
to `createFateServer`, and export the adapter next to your fate server.
|
|
2745
|
+
|
|
2746
|
+
```tsx
|
|
2747
|
+
import { createFateServer } from '@nkzw/fate/server';
|
|
2748
|
+
import { createDrizzleSourceAdapter } from '@nkzw/fate/server/drizzle';
|
|
2749
|
+
import { createVoidFateLive } from 'void-fate/server';
|
|
2750
|
+
import { db } from 'void/db';
|
|
2751
|
+
import schema from '../db/schema.ts';
|
|
2752
|
+
import { createContext } from './context.ts';
|
|
2753
|
+
import { Root } from './views.ts';
|
|
2754
|
+
|
|
2755
|
+
const sources = createDrizzleSourceAdapter({
|
|
2756
|
+
db,
|
|
2757
|
+
schema,
|
|
2758
|
+
views: Root,
|
|
2759
|
+
});
|
|
2760
|
+
|
|
2761
|
+
export const fateLive = createVoidFateLive();
|
|
2762
|
+
export const { live } = fateLive;
|
|
2763
|
+
|
|
2764
|
+
export const fateServer = createFateServer({
|
|
2765
|
+
context: ({ request }) => createContext({ request }),
|
|
2766
|
+
live,
|
|
2767
|
+
roots: Root,
|
|
2768
|
+
sources,
|
|
2769
|
+
});
|
|
2770
|
+
```
|
|
2771
|
+
|
|
2772
|
+
Your app can publish live updates through the normal fate live bus:
|
|
2773
|
+
|
|
2774
|
+
```tsx
|
|
2775
|
+
live.update('Post', postId, { changed: ['likes'] });
|
|
2776
|
+
live.connection('Post.comments', { id: postId }).appendNode('Comment', commentId, {
|
|
2777
|
+
node: comment,
|
|
2778
|
+
});
|
|
2779
|
+
```
|
|
2780
|
+
|
|
2781
|
+
`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.
|
|
2782
|
+
|
|
2783
|
+
### Routes
|
|
2784
|
+
|
|
2785
|
+
Add one route for fate RPC requests:
|
|
2786
|
+
|
|
2787
|
+
```tsx
|
|
2788
|
+
// routes/fate.ts
|
|
2789
|
+
import { defineVoidFateRoute } from 'void-fate/server';
|
|
2790
|
+
import { fateLive, fateServer } from '../src/fate/server.ts';
|
|
2791
|
+
|
|
2792
|
+
export const { GET, POST } = defineVoidFateRoute(fateServer, fateLive);
|
|
2793
|
+
```
|
|
2794
|
+
|
|
2795
|
+
Add a second route for the live SSE transport:
|
|
2796
|
+
|
|
2797
|
+
```tsx
|
|
2798
|
+
// routes/fate-live.ts
|
|
2799
|
+
import { defineVoidFateLiveRoute } from 'void-fate/server';
|
|
2800
|
+
import { fateLive, fateServer } from '../src/fate/server.ts';
|
|
2801
|
+
|
|
2802
|
+
export const { GET, POST } = defineVoidFateLiveRoute(fateServer, fateLive);
|
|
2803
|
+
```
|
|
2804
|
+
|
|
2805
|
+
The live route handles `GET /fate-live` SSE connections and `POST /fate-live`
|
|
2806
|
+
control messages. `void-fate` does not use WebSockets.
|
|
2807
|
+
|
|
2808
|
+
### Layout
|
|
2809
|
+
|
|
2810
|
+
Wrap your app with the Void fate client for your framework. It creates and
|
|
2811
|
+
provides the fate client through the matching adapter.
|
|
2812
|
+
|
|
2813
|
+
::: code-group
|
|
2814
|
+
|
|
2815
|
+
```tsx [React]
|
|
2816
|
+
import { useShared } from '@void/react';
|
|
2817
|
+
import type { ReactNode } from 'react';
|
|
2818
|
+
import { VoidFateClient } from 'void-fate/react';
|
|
2819
|
+
import type { SharedData } from '../src/lib/shared.ts';
|
|
2820
|
+
|
|
2821
|
+
export default function Layout({ children }: { children: ReactNode }) {
|
|
2822
|
+
const shared = useShared<SharedData>();
|
|
2823
|
+
const userId = shared.auth.user?.id;
|
|
2824
|
+
const origin = typeof window === 'undefined' ? shared.origin : window.location.origin;
|
|
2825
|
+
|
|
2826
|
+
return (
|
|
2827
|
+
<VoidFateClient origin={origin} userId={userId}>
|
|
2828
|
+
{children}
|
|
2829
|
+
</VoidFateClient>
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
```
|
|
2833
|
+
|
|
2834
|
+
```vue [Vue]
|
|
2835
|
+
<script setup lang="ts">
|
|
2836
|
+
import { useShared } from '@void/vue';
|
|
2837
|
+
import { computed } from 'vue';
|
|
2838
|
+
import { FateClient } from 'vue-fate';
|
|
2839
|
+
import { createFateClient } from 'vue-fate/client';
|
|
2840
|
+
import type { SharedData } from '../src/lib/shared.ts';
|
|
2841
|
+
|
|
2842
|
+
const shared = useShared<SharedData>();
|
|
2843
|
+
|
|
2844
|
+
const fate = computed(() =>
|
|
2845
|
+
createFateClient({
|
|
2846
|
+
origin: typeof window === 'undefined' ? shared.origin : window.location.origin,
|
|
2847
|
+
userId: shared.auth.user?.id,
|
|
2848
|
+
}),
|
|
2849
|
+
);
|
|
2850
|
+
</script>
|
|
2851
|
+
|
|
2852
|
+
<template>
|
|
2853
|
+
<FateClient :client="fate">
|
|
2854
|
+
<slot />
|
|
2855
|
+
</FateClient>
|
|
2856
|
+
</template>
|
|
2857
|
+
```
|
|
2858
|
+
|
|
2859
|
+
:::
|
|
2860
|
+
|
|
2861
|
+
`userId` is optional, but passing it lets the client be recreated when the
|
|
2862
|
+
signed-in user changes. Browser requests include credentials when a `userId` is
|
|
2863
|
+
present.
|
|
2864
|
+
|
|
2865
|
+
### Custom Paths
|
|
2866
|
+
|
|
2867
|
+
The default route pair is `/fate` and `/fate-live`. If your Void app uses
|
|
2868
|
+
different paths, configure the same values on the live adapter and client.
|
|
2869
|
+
|
|
2870
|
+
```tsx
|
|
2871
|
+
export const fateLive = createVoidFateLive({
|
|
2872
|
+
livePath: '/custom-fate-live',
|
|
2873
|
+
});
|
|
2874
|
+
```
|
|
2875
|
+
|
|
2876
|
+
::: code-group
|
|
2877
|
+
|
|
2878
|
+
```tsx [React]
|
|
2879
|
+
<VoidFateClient livePath="/custom-fate-live" origin={origin} rpcPath="/custom-fate" userId={userId}>
|
|
2880
|
+
{children}
|
|
2881
|
+
</VoidFateClient>
|
|
2882
|
+
```
|
|
2883
|
+
|
|
2884
|
+
```vue [Vue]
|
|
2885
|
+
<script setup lang="ts">
|
|
2886
|
+
const fate = computed(() =>
|
|
2887
|
+
createFateClient({
|
|
2888
|
+
livePath: '/custom-fate-live',
|
|
2889
|
+
origin,
|
|
2890
|
+
rpcPath: '/custom-fate',
|
|
2891
|
+
userId,
|
|
2892
|
+
}),
|
|
2893
|
+
);
|
|
2894
|
+
</script>
|
|
2895
|
+
|
|
2896
|
+
<template>
|
|
2897
|
+
<FateClient :client="fate">
|
|
2898
|
+
<slot />
|
|
2899
|
+
</FateClient>
|
|
2900
|
+
</template>
|
|
2901
|
+
```
|
|
2902
|
+
|
|
2903
|
+
:::
|
|
2904
|
+
|
|
2905
|
+
The route helper does not own the route path. Make sure your Void route filename
|
|
2906
|
+
or router configuration matches the paths you pass to the client.
|
|
2907
|
+
|
|
2908
|
+
### Live Transport
|
|
2909
|
+
|
|
2910
|
+
Void can run separate request handlers for mutations and long-lived SSE
|
|
2911
|
+
connections. `createVoidFateLive` bridges those handlers by publishing live
|
|
2912
|
+
events from the request that changed data to the live route.
|
|
2913
|
+
|
|
2914
|
+
In local development, `void-fate` uses a development token for that internal
|
|
2915
|
+
publish request. Outside local development, Void must provide `__VOID_PROXY_TOKEN`
|
|
2916
|
+
in the route environment. If no internal publish token is available, the adapter
|
|
2917
|
+
falls back to the in-memory live bus for the current request context.
|
|
2918
|
+
|
|
2919
|
+
The live transport is best-effort and does not replay missed events after a
|
|
2920
|
+
client reconnects. This matches fate's default in-memory live event bus.
|
|
2921
|
+
|
|
1849
2922
|
## Frequently Asked Questions
|
|
1850
2923
|
|
|
1851
2924
|
### Is this serious software?
|