@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Vue
|
|
2
|
+
|
|
3
|
+
_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.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install `vue-fate` in your Vue client:
|
|
10
|
+
|
|
11
|
+
::: code-group
|
|
12
|
+
|
|
13
|
+
```bash [npm]
|
|
14
|
+
npm add vue-fate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash [pnpm]
|
|
18
|
+
pnpm add vue-fate
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash [yarn]
|
|
22
|
+
yarn add vue-fate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
:::
|
|
26
|
+
|
|
27
|
+
If your server lives in a separate package, install `@nkzw/fate` there as a runtime dependency too.
|
|
28
|
+
|
|
29
|
+
## Vite Plugin
|
|
30
|
+
|
|
31
|
+
Use the Vue adapter's Vite plugin in the client app:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { fate } from 'vue-fate/vite';
|
|
35
|
+
import { defineConfig } from 'vite';
|
|
36
|
+
import vue from '@vitejs/plugin-vue';
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
plugins: [
|
|
40
|
+
vue(),
|
|
41
|
+
fate({
|
|
42
|
+
module: '@your-org/server/fate.ts',
|
|
43
|
+
transport: 'native',
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
## Providing the Client
|
|
52
|
+
|
|
53
|
+
Create a client with `createFateClient` and provide it with `FateClient`:
|
|
54
|
+
|
|
55
|
+
```vue
|
|
56
|
+
<script setup lang="ts">
|
|
57
|
+
import { computed, ref } from 'vue';
|
|
58
|
+
import { FateClient } from 'vue-fate';
|
|
59
|
+
import { createFateClient } from 'vue-fate/client';
|
|
60
|
+
import AppRoutes from './AppRoutes.vue';
|
|
61
|
+
|
|
62
|
+
const token = ref<string | null>(null);
|
|
63
|
+
|
|
64
|
+
const fate = computed(() =>
|
|
65
|
+
createFateClient({
|
|
66
|
+
headers: () => ({
|
|
67
|
+
authorization: token.value ? `Bearer ${token.value}` : '',
|
|
68
|
+
}),
|
|
69
|
+
url: '/fate',
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<FateClient :client="fate">
|
|
76
|
+
<Suspense>
|
|
77
|
+
<AppRoutes />
|
|
78
|
+
</Suspense>
|
|
79
|
+
</FateClient>
|
|
80
|
+
</template>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
You can also install the client as a Vue plugin:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { createApp } from 'vue';
|
|
89
|
+
import { createFatePlugin } from 'vue-fate';
|
|
90
|
+
import { createFateClient } from 'vue-fate/client';
|
|
91
|
+
import App from './App.vue';
|
|
92
|
+
|
|
93
|
+
const fate = createFateClient({ url: '/fate' });
|
|
94
|
+
|
|
95
|
+
createApp(App).use(createFatePlugin(fate)).mount('#app');
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Defining Views
|
|
99
|
+
|
|
100
|
+
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:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import type { Post, User } from '@your-org/server/views';
|
|
104
|
+
import { view } from 'vue-fate';
|
|
105
|
+
|
|
106
|
+
export const UserView = view<User>()({
|
|
107
|
+
id: true,
|
|
108
|
+
name: true,
|
|
109
|
+
username: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const PostView = view<Post>()({
|
|
113
|
+
author: UserView,
|
|
114
|
+
id: true,
|
|
115
|
+
title: true,
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
## Requests
|
|
122
|
+
|
|
123
|
+
`useRequest` declares the data a route, page, or component tree needs. It returns a resource with `data`, `pending`, `error`, `ready`, `refresh`, and `dispose`:
|
|
124
|
+
|
|
125
|
+
```vue
|
|
126
|
+
<script setup lang="ts">
|
|
127
|
+
import { useListView, useRequest } from 'vue-fate';
|
|
128
|
+
import { PostCardView } from '../fateViews';
|
|
129
|
+
import PostCard from '../ui/PostCard.vue';
|
|
130
|
+
|
|
131
|
+
const request = useRequest({
|
|
132
|
+
posts: {
|
|
133
|
+
args: { first: 20 },
|
|
134
|
+
list: PostCardView,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const { posts } = await request.ready();
|
|
139
|
+
const [postItems, loadNext] = useListView(PostCardView, posts);
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<template>
|
|
143
|
+
<PostCard v-for="{ node } in postItems" :key="node.id" :post="node" />
|
|
144
|
+
<button v-if="loadNext" @click="loadNext()">Load more</button>
|
|
145
|
+
</template>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
150
|
+
## Views in Components
|
|
151
|
+
|
|
152
|
+
Use `useView` to read a `ViewRef` from the normalized cache and subscribe to updates for the selected fields:
|
|
153
|
+
|
|
154
|
+
```vue
|
|
155
|
+
<script setup lang="ts">
|
|
156
|
+
import type { ViewRef } from 'vue-fate';
|
|
157
|
+
import { useView } from 'vue-fate';
|
|
158
|
+
import { PostCardView, UserView } from '../fateViews';
|
|
159
|
+
import UserCard from './UserCard.vue';
|
|
160
|
+
|
|
161
|
+
const props = defineProps<{
|
|
162
|
+
post: ViewRef<'Post'>;
|
|
163
|
+
}>();
|
|
164
|
+
|
|
165
|
+
const post = useView(PostCardView, () => props.post);
|
|
166
|
+
const author = useView(UserView, () => post.value?.author ?? null);
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<template>
|
|
170
|
+
<article v-if="post">
|
|
171
|
+
<h2>{{ post.title }}</h2>
|
|
172
|
+
<UserCard v-if="author" :user="author" />
|
|
173
|
+
</article>
|
|
174
|
+
</template>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
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.
|
|
178
|
+
|
|
179
|
+
## Lists and Live Views
|
|
180
|
+
|
|
181
|
+
`useListView` subscribes to a connection returned from `useRequest` or from a nested view field:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const [comments, loadNextCommentPage] = useListView(CommentView, () => post.value?.comments);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`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:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const post = useLiveView(PostCardView, () => props.post);
|
|
191
|
+
const [comments] = useLiveListView(CommentView, () => post.value?.comments);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Manual cleanup works through `dispose()`:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const post = useLiveView(PostCardView, () => props.post);
|
|
198
|
+
|
|
199
|
+
onBeforeUnmount(() => {
|
|
200
|
+
post.dispose();
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Vue scope disposal also cleans up resources automatically.
|
|
205
|
+
|
|
206
|
+
## Mutations
|
|
207
|
+
|
|
208
|
+
Use `useFateClient` to access generated mutations:
|
|
209
|
+
|
|
210
|
+
```vue
|
|
211
|
+
<script setup lang="ts">
|
|
212
|
+
import { ref } from 'vue';
|
|
213
|
+
import { useFateClient } from 'vue-fate';
|
|
214
|
+
|
|
215
|
+
const props = defineProps<{
|
|
216
|
+
post: { id: string; likes: number };
|
|
217
|
+
}>();
|
|
218
|
+
|
|
219
|
+
const fate = useFateClient();
|
|
220
|
+
const pending = ref(false);
|
|
221
|
+
const error = ref<unknown>(null);
|
|
222
|
+
|
|
223
|
+
const like = async () => {
|
|
224
|
+
pending.value = true;
|
|
225
|
+
error.value = null;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await fate.mutations.post.like({
|
|
229
|
+
input: { id: props.post.id },
|
|
230
|
+
optimistic: { likes: props.post.likes + 1 },
|
|
231
|
+
});
|
|
232
|
+
} catch (caughtError) {
|
|
233
|
+
error.value = caughtError;
|
|
234
|
+
} finally {
|
|
235
|
+
pending.value = false;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<template>
|
|
241
|
+
<button :disabled="pending" @click="like">Like</button>
|
|
242
|
+
</template>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
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.
|
|
246
|
+
|
|
247
|
+
## API Differences from React
|
|
248
|
+
|
|
249
|
+
The names intentionally mirror `react-fate` where Vue has an equivalent API. The main differences are Vue framework differences:
|
|
250
|
+
|
|
251
|
+
- `useRequest`, `useView`, and list hooks return Vue resources instead of throwing promises from render.
|
|
252
|
+
- Async setup and `<Suspense>` replace React's async component model.
|
|
253
|
+
- Mutations use `fate.mutations` directly; React-only `fate.actions` and `useActionState` patterns do not apply.
|
|
254
|
+
- Shared views are best kept in `.ts` modules instead of exporting named views from component files.
|
|
255
|
+
|
|
256
|
+
The generated client, server integrations, cache behavior, masking, pagination, optimistic updates, and live transport behavior are shared across adapters.
|
package/docs/index.md
CHANGED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# GraphQL Integration
|
|
2
|
+
|
|
3
|
+
_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.
|
|
4
|
+
|
|
5
|
+
Use the GraphQL transport when your backend already exposes GraphQL and you want fate's client model without adding fate's native server protocol.
|
|
6
|
+
|
|
7
|
+
## Template
|
|
8
|
+
|
|
9
|
+
Create a client for an existing GraphQL server with:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
vp create fate my-app --template graphql-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Create a full GraphQL + Prisma example app with:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
vp create fate my-app --template graphql
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
## GraphQL Schema Shape
|
|
24
|
+
|
|
25
|
+
The GraphQL transport expects a schema with Relay-style object identity and pagination:
|
|
26
|
+
|
|
27
|
+
- Entity objects include `id` and `__typename`.
|
|
28
|
+
- Object fetches go through a `nodes(ids:)` field.
|
|
29
|
+
- List fields return Relay connections with `edges`, `cursor`, `node`, and `pageInfo`.
|
|
30
|
+
- Root queries and mutations return the entity type selected by the fate view.
|
|
31
|
+
|
|
32
|
+
For example, a `Post` list can be exposed as a normal GraphQL connection:
|
|
33
|
+
|
|
34
|
+
```graphql
|
|
35
|
+
type Query {
|
|
36
|
+
posts(first: Int, after: String): PostConnection!
|
|
37
|
+
viewer: User
|
|
38
|
+
nodes(ids: [ID!]!): [Node]!
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type PostConnection {
|
|
42
|
+
edges: [PostEdge!]!
|
|
43
|
+
pageInfo: PageInfo!
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If your schema uses different root field names, keep the fate names you want on the client and map them with `fateGraphQL.roots`.
|
|
48
|
+
|
|
49
|
+
## Mapping Your Schema
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { graphqlMutation } from '@nkzw/fate';
|
|
55
|
+
import { dataView, list, type Entity } from '@nkzw/fate/server';
|
|
56
|
+
|
|
57
|
+
type GraphQLUser = {
|
|
58
|
+
id: string;
|
|
59
|
+
name?: string | null;
|
|
60
|
+
username?: string | null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type GraphQLPost = {
|
|
64
|
+
author?: GraphQLUser | null;
|
|
65
|
+
id: string;
|
|
66
|
+
title: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const userDataView = dataView<GraphQLUser>('User')({
|
|
70
|
+
id: true,
|
|
71
|
+
name: true,
|
|
72
|
+
username: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const postDataView = dataView<GraphQLPost>('Post')({
|
|
76
|
+
author: userDataView,
|
|
77
|
+
id: true,
|
|
78
|
+
title: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type User = Entity<typeof userDataView, 'User'>;
|
|
82
|
+
export type Post = Entity<
|
|
83
|
+
typeof postDataView,
|
|
84
|
+
'Post',
|
|
85
|
+
{
|
|
86
|
+
author: User | null;
|
|
87
|
+
}
|
|
88
|
+
>;
|
|
89
|
+
|
|
90
|
+
export const Root = {
|
|
91
|
+
posts: list(postDataView),
|
|
92
|
+
viewer: userDataView,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const fateGraphQL = {
|
|
96
|
+
roots: {
|
|
97
|
+
posts: { field: 'posts' },
|
|
98
|
+
viewer: { field: 'viewer' },
|
|
99
|
+
},
|
|
100
|
+
} as const;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
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.
|
|
104
|
+
|
|
105
|
+
## Vite Plugin
|
|
106
|
+
|
|
107
|
+
Configure the fate Vite plugin with the GraphQL transport and point it at the mapping module:
|
|
108
|
+
|
|
109
|
+
::: code-group
|
|
110
|
+
|
|
111
|
+
```tsx [React]
|
|
112
|
+
import { fate } from 'react-fate/vite';
|
|
113
|
+
import { defineConfig } from 'vite';
|
|
114
|
+
|
|
115
|
+
export default defineConfig({
|
|
116
|
+
plugins: [
|
|
117
|
+
fate({
|
|
118
|
+
module: './src/fate/graphql.ts',
|
|
119
|
+
transport: 'graphql',
|
|
120
|
+
}),
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```ts [Vue]
|
|
126
|
+
import vue from '@vitejs/plugin-vue';
|
|
127
|
+
import { fate } from 'vue-fate/vite';
|
|
128
|
+
import { defineConfig } from 'vite';
|
|
129
|
+
|
|
130
|
+
export default defineConfig({
|
|
131
|
+
plugins: [
|
|
132
|
+
vue(),
|
|
133
|
+
fate({
|
|
134
|
+
module: './src/fate/graphql.ts',
|
|
135
|
+
transport: 'graphql',
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
:::
|
|
142
|
+
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
## Creating a Client
|
|
146
|
+
|
|
147
|
+
Create the client with your GraphQL endpoint and provide it through the `FateClient` provider:
|
|
148
|
+
|
|
149
|
+
::: code-group
|
|
150
|
+
|
|
151
|
+
```tsx [React]
|
|
152
|
+
import { FateClient } from 'react-fate';
|
|
153
|
+
import { createFateClient } from 'react-fate/client';
|
|
154
|
+
|
|
155
|
+
const fate = createFateClient({
|
|
156
|
+
headers: () => ({
|
|
157
|
+
authorization: `Bearer ${token}`,
|
|
158
|
+
}),
|
|
159
|
+
url: 'https://api.example.com/graphql',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export function App() {
|
|
163
|
+
return <FateClient client={fate}>{/* Components go here */}</FateClient>;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```vue [Vue]
|
|
168
|
+
<script setup lang="ts">
|
|
169
|
+
import { FateClient } from 'vue-fate';
|
|
170
|
+
import { createFateClient } from 'vue-fate/client';
|
|
171
|
+
import AppRoutes from './AppRoutes.vue';
|
|
172
|
+
|
|
173
|
+
const fate = createFateClient({
|
|
174
|
+
headers: () => ({
|
|
175
|
+
authorization: `Bearer ${token}`,
|
|
176
|
+
}),
|
|
177
|
+
url: 'https://api.example.com/graphql',
|
|
178
|
+
});
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<template>
|
|
182
|
+
<FateClient :client="fate">
|
|
183
|
+
<AppRoutes />
|
|
184
|
+
</FateClient>
|
|
185
|
+
</template>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
:::
|
|
189
|
+
|
|
190
|
+
Use `fetch` when you need to customize credentials or reuse an application fetch wrapper:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
const fate = createFateClient({
|
|
194
|
+
fetch: (input, init) =>
|
|
195
|
+
fetch(input, {
|
|
196
|
+
...init,
|
|
197
|
+
credentials: 'include',
|
|
198
|
+
}),
|
|
199
|
+
url: `${env('SERVER_URL')}/graphql`,
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
GraphQL operations issued in the same microtask are batched into a single GraphQL query or mutation document with aliased fields.
|
|
204
|
+
|
|
205
|
+
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.
|
|
206
|
+
|
|
207
|
+
## Object IDs
|
|
208
|
+
|
|
209
|
+
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:
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
const fate = createFateClient({
|
|
213
|
+
decodeNodeId: (type, id) => {
|
|
214
|
+
const [nodeType, nodeId] = atob(String(id)).split(':');
|
|
215
|
+
if (nodeType !== type) {
|
|
216
|
+
throw new Error(`Expected a ${type} node id.`);
|
|
217
|
+
}
|
|
218
|
+
return nodeId;
|
|
219
|
+
},
|
|
220
|
+
encodeNodeId: (type, id) => btoa(`${type}:${id}`),
|
|
221
|
+
url: '/graphql',
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
If your GraphQL API already accepts and returns the same IDs you use in the app, return `id` from both functions.
|
|
226
|
+
|
|
227
|
+
## Requests and Arguments
|
|
228
|
+
|
|
229
|
+
Client code keeps using `useRequest` with the same shape as the other transports:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
const { posts, viewer } = useRequest({
|
|
233
|
+
posts: {
|
|
234
|
+
args: { first: 10 },
|
|
235
|
+
list: PostView,
|
|
236
|
+
},
|
|
237
|
+
viewer: { view: UserView },
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Root arguments are sent to the root GraphQL field. Nested relation arguments are scoped by relation name:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
const { posts } = useRequest({
|
|
245
|
+
posts: {
|
|
246
|
+
args: {
|
|
247
|
+
comments: { first: 3 },
|
|
248
|
+
first: 10,
|
|
249
|
+
},
|
|
250
|
+
list: PostWithCommentsView,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
This produces a root `posts(first: 10)` field and a nested `comments(first: 3)` field in the generated GraphQL selection.
|
|
256
|
+
|
|
257
|
+
## Mutations
|
|
258
|
+
|
|
259
|
+
Map fate mutation names to GraphQL mutation fields with `graphqlMutation`:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
export const fateGraphQL = {
|
|
263
|
+
mutations: {
|
|
264
|
+
'post.like': graphqlMutation<Post, { id: string }, Post>('Post', {
|
|
265
|
+
field: 'postLike',
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
roots: {
|
|
269
|
+
posts: { field: 'posts' },
|
|
270
|
+
},
|
|
271
|
+
} as const;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
By default, the input is sent as an `input` argument:
|
|
275
|
+
|
|
276
|
+
```graphql
|
|
277
|
+
mutation {
|
|
278
|
+
postLike(input: { id: "12" }) {
|
|
279
|
+
id
|
|
280
|
+
likes
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Use `inputArg` when your schema uses a different argument name, or `inputArg: false` when the input object should be spread into field arguments:
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
export const fateGraphQL = {
|
|
289
|
+
mutations: {
|
|
290
|
+
'post.like': graphqlMutation<Post, { id: string }, Post>('Post', {
|
|
291
|
+
field: 'likePost',
|
|
292
|
+
inputArg: 'payload',
|
|
293
|
+
}),
|
|
294
|
+
'user.follow': graphqlMutation<User, { id: string }, User>('User', {
|
|
295
|
+
field: 'followUser',
|
|
296
|
+
inputArg: false,
|
|
297
|
+
}),
|
|
298
|
+
},
|
|
299
|
+
} as const;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Mutations use the same `mutation(...)` API described in the [Actions Guide](../guide/actions.md). React clients can also expose those mutations as Actions for `useActionState`.
|
|
303
|
+
|
|
304
|
+
## Live Views
|
|
305
|
+
|
|
306
|
+
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.
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
const fate = createFateClient({
|
|
310
|
+
live: {
|
|
311
|
+
url: 'https://api.example.com/graphql/stream',
|
|
312
|
+
},
|
|
313
|
+
url: 'https://api.example.com/graphql',
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The default subscription fields are `fateLiveNode` for `useLiveView` and `fateLiveConnection` for `useLiveListView`. Rename them with `entityField` and `connectionField`:
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
const fate = createFateClient({
|
|
321
|
+
live: {
|
|
322
|
+
connectionField: 'liveConnection',
|
|
323
|
+
entityField: 'liveNode',
|
|
324
|
+
url: '/graphql/stream',
|
|
325
|
+
},
|
|
326
|
+
url: '/graphql',
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
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.
|
|
331
|
+
|
|
332
|
+
If you do not need live views, disable them explicitly:
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
const fate = createFateClient({
|
|
336
|
+
live: false,
|
|
337
|
+
url: '/graphql',
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Existing Servers
|
|
342
|
+
|
|
343
|
+
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.
|
|
344
|
+
|
|
345
|
+
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.
|