@nkzw/fate 0.0.8 → 0.1.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 CHANGED
@@ -18,7 +18,7 @@
18
18
  - **Async React:** fate uses modern Async React features like Actions, Suspense, and `use` to support concurrent rendering and enable a seamless user experience.
19
19
  - **Lists & Pagination:** fate provides built-in support for connection-style lists with cursor-based pagination, making it easy to implement infinite scrolling and "load-more" functionality.
20
20
  - **Optimistic Updates:** fate supports declarative optimistic updates for mutations, allowing the UI to update immediately while the server request is in-flight. If the request fails, the cache and its associated views are rolled back to their previous state.
21
- - **AI-Ready:** fate's minimal, predictable API and explicit data selection enable local reasoning, allowing AI tools to generate stable, type-safe data-fetching code.
21
+ - **AI-Ready:** fate's minimal, predictable API and explicit data selection enable local reasoning, enabling humans and AI tools to generate stable, type-safe data-fetching code.
22
22
 
23
23
  ### A modern data client for React & tRPC
24
24
 
@@ -32,14 +32,14 @@ However, GraphQL comes with its own type system and query language. If you are a
32
32
 
33
33
  Many React data frameworks lack Relay's ergonomics, especially fragment composition, co-located data requirements, predictable caching, and deep integration with modern React features. Optimistic updates usually require manually managing keys and imperative data updates, which is error-prone and tedious.
34
34
 
35
- fate takes the great ideas from Relay and puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching. Using _fate_ usually looks like this:
35
+ _fate_ takes the great ideas from Relay and puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching. Using _fate_ usually looks like this:
36
36
 
37
37
  ```tsx
38
38
  export const PostView = view<Post>()({
39
+ author: UserView,
39
40
  content: true,
40
41
  id: true,
41
42
  title: true,
42
- author: UserView,
43
43
  });
44
44
 
45
45
  export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
@@ -65,15 +65,15 @@ Get started with [a ready-made template](https://github.com/nkzw-tech/fate-templ
65
65
 
66
66
  ::: code-group
67
67
 
68
- ```npm
68
+ ```bash [npm]
69
69
  npx giget@latest gh:nkzw-tech/fate-template
70
70
  ```
71
71
 
72
- ```pnpm
72
+ ```bash [pnpm]
73
73
  pnpx giget@latest gh:nkzw-tech/fate-template
74
74
  ```
75
75
 
76
- ```yarn
76
+ ```bash [yarn]
77
77
  yarn dlx giget@latest gh:nkzw-tech/fate-template
78
78
  ```
79
79
 
@@ -87,15 +87,15 @@ yarn dlx giget@latest gh:nkzw-tech/fate-template
87
87
 
88
88
  ::: code-group
89
89
 
90
- ```npm
90
+ ```bash [npm]
91
91
  npm add react-fate
92
92
  ```
93
93
 
94
- ```pnpm
94
+ ```bash [pnpm]
95
95
  pnpm add react-fate
96
96
  ```
97
97
 
98
- ```yarn
98
+ ```bash [yarn]
99
99
  yarn add react-fate
100
100
  ```
101
101
 
@@ -105,15 +105,15 @@ And for your server, install the core `@nkzw/fate` package:
105
105
 
106
106
  ::: code-group
107
107
 
108
- ```npm
108
+ ```bash [npm]
109
109
  npm add @nkzw/fate
110
110
  ```
111
111
 
112
- ```pnpm
112
+ ```bash [pnpm]
113
113
  pnpm add @nkzw/fate
114
114
  ```
115
115
 
116
- ```yarn
116
+ ```bash [yarn]
117
117
  yarn add @nkzw/fate
118
118
  ```
119
119
 
@@ -202,34 +202,20 @@ Components using `useView` listen to changes for all selected fields. When data
202
202
 
203
203
  ### Fetching Data with `useRequest`
204
204
 
205
- Now that we defined our view and component, we fetch the data from the server using the `useRequest` hook from fate. This hook allows us to declare what data we need for a specific screen or component tree. At the root of our `HomePage` component, we can request a list of posts like this:
205
+ Now that we defined our view and component, we fetch the data from the server using the `useRequest` hook from fate. This hook allows us to declare what data we need for a specific screen or component tree. At the root of our app, we can request a list of posts like this:
206
206
 
207
207
  ```tsx
208
208
  import { useRequest } from 'react-fate';
209
209
  import { PostCard, PostView } from './PostCard.tsx';
210
210
 
211
- export function HomePage() {
212
- const { posts } = useRequest({
213
- posts: { root: PostView, type: 'Post' },
214
- } as const);
211
+ export function App() {
212
+ const { posts } = useRequest({ posts: { list: PostView } });
215
213
 
216
214
  return posts.map((post) => <PostCard key={post.id} post={post} />);
217
215
  }
218
216
  ```
219
217
 
220
- This component suspends or throws errors, which bubble up to the nearest error boundary. Wrap your component tree with `ErrorBoundary` and `Suspense` components to show error and loading states:
221
-
222
- ```tsx
223
- <ErrorBoundary FallbackComponent={ErrorComponent}>
224
- <Suspense fallback={<div>Loading…</div>}>
225
- <HomePage />
226
- </Suspense>
227
- </ErrorBoundary>
228
- ```
229
-
230
- > [!NOTE]
231
- >
232
- > `useRequest` might issue multiple requests which are automatically batched together by tRPC's [HTTP Batch Link](https://trpc.io/docs/client/links/httpBatchLink).
218
+ _Learn more about `useRequest` in the [Requests Guide](/docs/guide/requests.md)._
233
219
 
234
220
  ### Composing Views
235
221
 
@@ -237,7 +223,7 @@ In the above example we are defining a single view for a `Post`. One of fate's c
237
223
 
238
224
  ```tsx
239
225
  import { Suspense } from 'react';
240
- import { useRequest, useView, ViewRef } from 'react-fate';
226
+ import { useView, ViewRef } from 'react-fate';
241
227
 
242
228
  export const PostView = view<Post>()({
243
229
  author: {
@@ -443,23 +429,62 @@ const PostDetail = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
443
429
 
444
430
  ViewRefs carry a set of view names they can resolve. `useView` throws if a ref does not include the required view.
445
431
 
446
- ### Request Modes
432
+ ## Requests
447
433
 
448
- `useRequest` supports different request modes to control caching and data freshness. The available modes are:
434
+ ### Requesting Lists
449
435
 
450
- - `cache-first` (_default_): Returns data from the cache if available, otherwise fetches from the network.
451
- - `stale-while-revalidate`: Returns data from the cache and simultaneously fetches fresh data from the network.
452
- - `network-only`: Always fetches data from the network, bypassing the cache.
436
+ The `useRequest` hook can be used to declare our data needs for a specific screen or component tree. At the root of our app, we can request a list of posts like this:
453
437
 
454
- You can pass the request mode as an option to `useRequest`:
438
+ ```tsx
439
+ import { useRequest } from 'react-fate';
440
+ import { PostCard, PostView } from './PostCard.tsx';
441
+
442
+ export function App() {
443
+ const { posts } = useRequest({ posts: { list: PostView } });
444
+ return posts.map((post) => <PostCard key={post.id} post={post} />);
445
+ }
446
+ ```
447
+
448
+ This component suspends or throws errors, which bubble up to the nearest error boundary. Wrap your component tree with `ErrorBoundary` and `Suspense` components to show error and loading states:
455
449
 
456
450
  ```tsx
457
- const { posts } = useRequest(
458
- {
459
- posts: { root: PostView, type: 'Post' },
460
- },
461
- { mode: 'stale-while-revalidate' },
462
- );
451
+ <ErrorBoundary FallbackComponent={ErrorComponent}>
452
+ <Suspense fallback={<div>Loading…</div>}>
453
+ <App />
454
+ </Suspense>
455
+ </ErrorBoundary>
456
+ ```
457
+
458
+ > [!NOTE]
459
+ >
460
+ > `useRequest` might issue multiple requests which are automatically batched together by tRPC's [HTTP Batch Link](https://trpc.io/docs/client/links/httpBatchLink) into a single network request.
461
+
462
+ ### Requesting Objects by ID
463
+
464
+ If you want to fetch data for a single object instead of a list, you can specify the `id` and the associated `view` like this:
465
+
466
+ ```tsx
467
+ const { post } = useRequest({
468
+ post: { id: '12', view: PostView },
469
+ });
470
+ ```
471
+
472
+ If you want to fetch multiple objects by their IDs, you can use the `ids` field:
473
+
474
+ ```tsx
475
+ const { posts } = useRequest({
476
+ posts: { ids: ['6', '7'], view: PostView },
477
+ });
478
+ ```
479
+
480
+ ### Other Types of Requests
481
+
482
+ For any other queries, pass only the `type` and `view`:
483
+
484
+ ```tsx
485
+ const { viewer } = useRequest({
486
+ viewer: { view: UserView },
487
+ });
463
488
  ```
464
489
 
465
490
  ### Request Arguments
@@ -470,12 +495,32 @@ You can pass arguments to `useRequest` calls. This is useful for pagination, fil
470
495
  const { posts } = useRequest({
471
496
  posts: {
472
497
  args: { first: 10 },
473
- root: PostView,
474
- type: 'Post',
498
+ list: PostView,
475
499
  },
476
500
  });
477
501
  ```
478
502
 
503
+ ### Request Modes
504
+
505
+ `useRequest` supports different request modes to control caching and data freshness. The available modes are:
506
+
507
+ - `cache-first` (_default_): Returns data from the cache if available, otherwise fetches from the network.
508
+ - `stale-while-revalidate`: Returns data from the cache and simultaneously fetches fresh data from the network.
509
+ - `network-only`: Always fetches data from the network, bypassing the cache.
510
+
511
+ You can pass the request mode as an option to `useRequest`:
512
+
513
+ ```tsx
514
+ const { posts } = useRequest(
515
+ {
516
+ posts: { list: PostView },
517
+ },
518
+ {
519
+ mode: 'stale-while-revalidate',
520
+ },
521
+ );
522
+ ```
523
+
479
524
  ## List Views
480
525
 
481
526
  ### Pagination with `useListView`
@@ -497,7 +542,7 @@ const CommentConnectionView = {
497
542
  items: {
498
543
  node: CommentView,
499
544
  },
500
- } as const;
545
+ };
501
546
 
502
547
  const PostView = view<Post>()({
503
548
  comments: CommentConnectionView,
@@ -915,7 +960,7 @@ export const postDataView = dataView<PostItem>('Post')({
915
960
  content: true,
916
961
  id: true,
917
962
  title: true,
918
- } as const;
963
+ });
919
964
  ```
920
965
 
921
966
  ### Data View Lists
@@ -950,6 +995,25 @@ export const Root = {
950
995
 
951
996
  Entries that wrap their view in `list(...)` are treated as list resolvers and use the `procedure` name when calling the corresponding router procedure, defaulting to `list`. If you omit `list(...)`, fate treats the entry as a standard query and uses the view type name to infer the router name.
952
997
 
998
+ For the above `Root` definitions, you can make the following requests using `useRequest`:
999
+
1000
+ ```tsx
1001
+ const query = 'Apple';
1002
+
1003
+ const { posts, categories, viewer } = useRequest({
1004
+ // Explicit Root queries:
1005
+ categories: { list: categoryView },
1006
+ commentSearch: { args: { query }, list: commentView },
1007
+ events: { list: eventView },
1008
+ posts: { list: postView },
1009
+ viewer: { view: userView },
1010
+
1011
+ // Queries by id, if those entities have a `byId` query defined:
1012
+ post: { id: '12', view: postView },
1013
+ comment: { ids: ['6', '7'], view: commentView },
1014
+ });
1015
+ ```
1016
+
953
1017
  ### Data View Resolvers
954
1018
 
955
1019
  fate data views support resolvers for computed fields. If we want to add a `commentCount` field to our `Post` data view, we can use the `resolver` helper that defines a Prisma selection for the database query together with a `resolve` function:
@@ -1015,34 +1079,30 @@ _Note: fate uses the specified server module name to extract the server types it
1015
1079
 
1016
1080
  ### Creating a _fate_ Client
1017
1081
 
1018
- Now that we have generated the client types, all that remains is creating the instance of the fate client, and using it in our React app using the `FateClient` context provider.
1019
-
1020
- Create a `fate.ts` file:
1021
-
1022
- ```tsx
1023
- import { createFateClient } from './lib/fate.generated';
1024
-
1025
- export const fate = createFateClient({
1026
- links: [
1027
- httpBatchLink({
1028
- fetch: (input, init) =>
1029
- fetch(input, {
1030
- ...init,
1031
- credentials: 'include',
1032
- }),
1033
- url: `${env('SERVER_URL')}/trpc`,
1034
- }),
1035
- ],
1036
- });
1037
- ```
1038
-
1039
- Now wrap your app with the `FateClient` provider:
1082
+ Now that we have generated the client types, all that remains is creating an instance of the fate client, and using it in our React app using the `FateClient` context provider:
1040
1083
 
1041
1084
  ```tsx
1085
+ import { httpBatchLink } from '@trpc/client';
1042
1086
  import { FateClient } from 'react-fate';
1043
- import { fate } from './fate.ts';
1087
+ import { createFateClient } from './fate.ts';
1044
1088
 
1045
1089
  export function App() {
1090
+ const fate = useMemo(
1091
+ () =>
1092
+ createFateClient({
1093
+ links: [
1094
+ httpBatchLink({
1095
+ fetch: (input, init) =>
1096
+ fetch(input, {
1097
+ ...init,
1098
+ credentials: 'include',
1099
+ }),
1100
+ url: `${env('SERVER_URL')}/trpc`,
1101
+ }),
1102
+ ],
1103
+ }),
1104
+ [],
1105
+ );
1046
1106
  return <FateClient client={fate}>{/* Components go here */}</FateClient>;
1047
1107
  }
1048
1108
  ```
@@ -1076,14 +1136,14 @@ Probably. One day. _Maybe._
1076
1136
 
1077
1137
  ## Future
1078
1138
 
1079
- **_fate_** is not complete yet. It lacks core features such as garbage collection, a compiler to extract view definitions statically and ahead of time, and there is too much backend boilerplate. The current implementation of _fate_ is not tied to tRPC or Prisma, those are just the ones we are starting with. We welcome contributions and ideas to improve fate. Here are some features we'd like to add:
1139
+ **_fate_** is not complete yet. The library lacks core features such as garbage collection, a compiler to extract view definitions statically ahead of time, and there is too much backend boilerplate. The current implementation of _fate_ is not tied to tRPC or Prisma, those are just the ones we are starting with. We welcome contributions and ideas to improve fate. Here are some features we'd like to add:
1080
1140
 
1081
1141
  - Support for Drizzle
1082
1142
  - Support backends other than tRPC
1143
+ - Persistent storage for offline support
1144
+ - Implement garbage collection for the cache
1083
1145
  - Better code generation and less type repetition
1084
1146
  - Support for live views and real-time updates via `useLiveView` and SSE
1085
- - Implement garbage collection for the cache
1086
- - Add persistent storage for offline support
1087
1147
 
1088
1148
  ## Acknowledgements
1089
1149
 
package/lib/cli.mjs CHANGED
@@ -135,7 +135,8 @@ const generate = async () => {
135
135
  if (procedureName === root$1.procedure && type === "query") queryEntries.push({
136
136
  name: queryName,
137
137
  procedure: root$1.procedure,
138
- router
138
+ router,
139
+ type: root$1.type
139
140
  });
140
141
  }
141
142
  for (const [listName, root$1] of routerRoots) {
@@ -143,7 +144,8 @@ const generate = async () => {
143
144
  if (procedureName === root$1.procedure && type === "query") listEntries.push({
144
145
  list: listName,
145
146
  procedure: root$1.procedure,
146
- router
147
+ router,
148
+ type: root$1.type
147
149
  });
148
150
  }
149
151
  }
@@ -152,6 +154,24 @@ const generate = async () => {
152
154
  byIdEntries.sort((a, b) => a.entityType.localeCompare(b.entityType));
153
155
  queryEntries.sort((a, b) => a.name.localeCompare(b.name));
154
156
  listEntries.sort((a, b) => a.list.localeCompare(b.list));
157
+ const rootEntries = [
158
+ ...byIdEntries.map(({ entityType, router }) => ({
159
+ name: router,
160
+ type: entityType,
161
+ value: `'${router}': clientRoot<RouterOutputs['${router}']['byId'], '${entityType}'>('${entityType}'),`
162
+ })),
163
+ ...queryEntries.map(({ name, procedure, router, type }) => ({
164
+ name,
165
+ type,
166
+ value: `'${name}': clientRoot<RouterOutputs['${router}']['${procedure}'], '${type}'>('${type}'),`
167
+ })),
168
+ ...listEntries.map(({ list, procedure, router, type }) => ({
169
+ name: list,
170
+ type,
171
+ value: `'${list}': clientRoot<RouterOutputs['${router}']['${procedure}'], '${type}'>('${type}'),`
172
+ }))
173
+ ];
174
+ rootEntries.sort((a, b) => a.name.localeCompare(b.name));
155
175
  const viewTypes = Array.from(["AppRouter", ...new Set(mutationEntries.map((entry) => entry.entityType))]).sort();
156
176
  const mutationResolverLines = mutationEntries.map(({ name, procedure, router }) => `'${name}': (client: TRPCClientType) => client.${router}.${procedure}.mutate,`);
157
177
  const mutationConfigLines = mutationEntries.map(({ entityType, name, procedure, router }) => `'${name}': mutation<
@@ -176,6 +196,7 @@ const generate = async () => {
176
196
  const mutationResolverBlock = indentBlock(mutationResolverLines.join("\n"), 4);
177
197
  const mutationConfigBlock = indentBlock(mutationConfigLines.join("\n"), 2);
178
198
  const byIdBlock = indentBlock(byIdLines.join("\n"), 8);
199
+ const rootsBlock = indentBlock(rootEntries.map((entry) => entry.value).join("\n"), 2);
179
200
  const listsBlockContent = listLines.join("\n");
180
201
  const listsBlock = listLines.length ? ` lists: {\n${indentBlock(listsBlockContent, 8)}\n },\n` : "";
181
202
  const queriesBlockContent = queryLines.join("\n");
@@ -183,7 +204,7 @@ const generate = async () => {
183
204
  ${typeImports}
184
205
  import { createTRPCProxyClient } from '@trpc/client';
185
206
  import { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
186
- import { createClient, createTRPCTransport, mutation } from 'react-fate';
207
+ import { clientRoot, createClient, createTRPCTransport, mutation } from 'react-fate';
187
208
 
188
209
  type TRPCClientType = ReturnType<typeof createTRPCProxyClient<AppRouter>>;
189
210
  type RouterInputs = inferRouterInputs<AppRouter>;
@@ -193,10 +214,16 @@ const mutations = {
193
214
  ${mutationConfigBlock}
194
215
  } as const;
195
216
 
217
+ const roots = {
218
+ ${rootsBlock}
219
+ } as const;
220
+
196
221
  type GeneratedClientMutations = typeof mutations;
222
+ type GeneratedClientRoots = typeof roots;
197
223
 
198
224
  declare module 'react-fate' {
199
225
  interface ClientMutations extends GeneratedClientMutations {}
226
+ interface ClientRoots extends GeneratedClientRoots {}
200
227
  }
201
228
 
202
229
  export const createFateClient = (options: {
@@ -208,8 +235,9 @@ export const createFateClient = (options: {
208
235
  ${mutationResolverBlock}
209
236
  } as const;
210
237
 
211
- return createClient({
238
+ return createClient<[GeneratedClientRoots, GeneratedClientMutations]>({
212
239
  mutations,
240
+ roots,
213
241
  transport: createTRPCTransport<AppRouter, typeof trpcMutations>({
214
242
  byId: {
215
243
  ${byIdBlock}
package/lib/index.d.mts CHANGED
@@ -1,14 +1,19 @@
1
- import { A as __FateEntityBrand, B as createTRPCTransport, C as ViewData, D as ViewSelection, E as ViewRef, F as FateClient, I as RequestMode, L as RequestOptions, M as isViewTag, N as FateMutations, O as ViewSnapshot, P as mutation, R as createClient, S as View, T as ViewEntityName, V as getSelectionPlan, _ as RequestResult, a as Entity, b as TypeConfig, c as Mask, d as MutationIdentifier, f as MutationInput, g as Request, h as Pagination, i as ConnectionTag, j as __FateSelectionBrand, k as ViewTag, l as MutationDefinition, m as NodesItem, n as ConnectionMetadata, o as EntityId, p as MutationResult, r as ConnectionRef, s as ListItem, t as AnyRecord, u as MutationEntity, v as Selection, w as ViewEntity, x as TypeName, y as Snapshot, z as Transport } from "./types-BPmcnouE.mjs";
1
+ import { A as ViewSelection, B as RequestOptions, C as TypeConfig, D as ViewEntity, E as ViewData, F as isViewTag, H as Transport, I as FateMutations, L as mutation, M as ViewTag, N as __FateEntityBrand, O as ViewEntityName, P as __FateSelectionBrand, R as FateClient, S as Snapshot, T as View, U as createTRPCTransport, V as createClient, W as getSelectionPlan, _ as Pagination, a as Entity, b as RootDefinition, c as FateThenable, d as MutationDefinition, f as MutationEntity, g as NodesItem, h as MutationResult, i as ConnectionTag, j as ViewSnapshot, k as ViewRef, l as ListItem, m as MutationInput, n as ConnectionMetadata, o as EntityId, p as MutationIdentifier, r as ConnectionRef, s as FateRoots, t as AnyRecord, u as Mask, v as Request, w as TypeName, x as Selection, y as RequestResult, z as RequestMode } from "./types-CNKGKRtz.mjs";
2
2
 
3
- //#region src/ref.d.ts
3
+ //#region src/root.d.ts
4
4
 
5
+ /**
6
+ * Defines a root query for an entity type, capturing the response shape.
7
+ */
8
+ declare function clientRoot<Result, Type extends TypeName>(type: Type): RootDefinition<Type, Result>;
9
+ //#endregion
10
+ //#region src/ref.d.ts
5
11
  /**
6
12
  * Builds the canonical cache ID for an entity.
7
13
  */
8
14
  declare const toEntityId: (type: TypeName, rawId: string | number) => EntityId;
9
15
  //#endregion
10
16
  //#region src/view.d.ts
11
-
12
17
  type SelectionValidation<T extends Entity, S extends Selection<T>> = Exclude<keyof Omit<S, typeof __FateEntityBrand | typeof __FateSelectionBrand>, keyof Selection<T>> extends never ? unknown : never;
13
18
  /**
14
19
  * Creates a reusable view for an object using the declared selection.
@@ -21,4 +26,4 @@ type SelectionValidation<T extends Entity, S extends Selection<T>> = Exclude<key
21
26
  */
22
27
  declare function view<T extends Entity>(): <S extends Selection<T>>(select: S & SelectionValidation<T, S>) => View<T, S>;
23
28
  //#endregion
24
- export { type ConnectionMetadata, type ConnectionRef, ConnectionTag, type Entity, type EntityId, FateClient, type FateMutations, type AnyRecord as FateRecord, type ListItem, type Mask, type MutationDefinition, type MutationEntity, type MutationIdentifier, type MutationInput, type MutationResult, type NodesItem, type Pagination, type Request, type RequestMode, type RequestOptions, type RequestResult, type Selection, type Snapshot, type Transport, type TypeConfig, type View, type ViewData, type ViewEntity, type ViewEntityName, type ViewRef, type ViewSelection, type ViewSnapshot, type ViewTag, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
29
+ export { type ConnectionMetadata, type ConnectionRef, ConnectionTag, type Entity, type EntityId, FateClient, type FateMutations, type AnyRecord as FateRecord, type FateRoots, type FateThenable, type ListItem, type Mask, type MutationDefinition, type MutationEntity, type MutationIdentifier, type MutationInput, type MutationResult, type NodesItem, type Pagination, type Request, type RequestMode, type RequestOptions, type RequestResult, type Selection, type Snapshot, type Transport, type TypeConfig, type View, type ViewData, type ViewEntity, type ViewEntityName, type ViewRef, type ViewSelection, type ViewSnapshot, type ViewTag, clientRoot, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
package/lib/index.mjs CHANGED
@@ -422,6 +422,8 @@ function isNodesItem(item) {
422
422
  function isQueryItem(item) {
423
423
  return "view" in item && !("id" in item) && !("ids" in item);
424
424
  }
425
+ /** Brand used on root definitions to mark their identity in the d.ts output. */
426
+ const RootKind = "__fate__root";
425
427
  /** Brand used on mutation definitions to mark their identity in the d.ts output. */
426
428
  const MutationKind = "__fate__mutation";
427
429
 
@@ -1006,18 +1008,18 @@ const getRequestCacheKey = (request) => {
1006
1008
  const item = request[name];
1007
1009
  if (!item) continue;
1008
1010
  if (isNodeItem(item)) {
1009
- parts.push(`node:${name}:${item.type}:${getViewSignature(item.view)}:${item.id}`);
1011
+ parts.push(`node:${name}:${getViewSignature(item.view)}:${item.id}`);
1010
1012
  continue;
1011
1013
  }
1012
1014
  if (isNodesItem(item)) {
1013
- parts.push(`node:${name}:${item.type}:${getViewSignature(item.view)}:${item.ids.map(serializeId).join(",")}`);
1015
+ parts.push(`node:${name}:${getViewSignature(item.view)}:${item.ids.map(serializeId).join(",")}`);
1014
1016
  continue;
1015
1017
  }
1016
1018
  if (isQueryItem(item)) {
1017
- parts.push(`query:${name}:${item.type}:${getViewSignature(item.view)}:${item.args ? hashArgs(item.args) : ""}`);
1019
+ parts.push(`query:${name}:${getViewSignature(item.view)}:${item.args ? hashArgs(item.args) : ""}`);
1018
1020
  continue;
1019
1021
  }
1020
- parts.push(`list:${name}:${item.type}:${getViewSignature(item.list)}:${item.args ? hashArgs(item.args) : ""}`);
1022
+ parts.push(`list:${name}:${getViewSignature(item.list)}:${item.args ? hashArgs(item.args) : ""}`);
1021
1023
  }
1022
1024
  return parts.join("$");
1023
1025
  };
@@ -1055,14 +1057,15 @@ var FateClient = class {
1055
1057
  this.stalledRequests = /* @__PURE__ */ new Set();
1056
1058
  this.store = new Store();
1057
1059
  this.viewDataCache = new ViewDataCache();
1060
+ this.actions = Object.create(null);
1061
+ this.mutationMap = Object.create(null);
1062
+ this.mutations = Object.create(null);
1063
+ this.roots = options.roots;
1058
1064
  this.transport = options.transport;
1059
1065
  this.types = new Map(options.types.map((entity) => [entity.type, {
1060
1066
  getId,
1061
1067
  ...entity
1062
1068
  }]));
1063
- this.mutationMap = Object.create(null);
1064
- this.mutations = Object.create(null);
1065
- this.actions = Object.create(null);
1066
1069
  if (options.mutations) for (const [key, definition] of Object.entries(options.mutations)) {
1067
1070
  const mutation$1 = wrapMutation(this, {
1068
1071
  ...definition,
@@ -1423,30 +1426,34 @@ var FateClient = class {
1423
1426
  this.executeRequest(request, { fetchAll: true }).catch(() => {});
1424
1427
  return result;
1425
1428
  }
1429
+ getRootType(name) {
1430
+ const root = this.roots[name];
1431
+ if (!root) throw new Error(`fate: Unknown root request '${name}'.`);
1432
+ return root.type;
1433
+ }
1426
1434
  async executeRequest(request, options = {}) {
1427
1435
  const fetchAll = options.fetchAll ?? false;
1428
1436
  const groups = /* @__PURE__ */ new Map();
1429
1437
  const promises = [];
1430
1438
  for (const [name, item] of Object.entries(request)) {
1439
+ const type = this.getRootType(name);
1431
1440
  const isNode = isNodeItem(item);
1432
1441
  if (isNode || isNodesItem(item)) {
1433
1442
  const plan = getSelectionPlan(item.view, null);
1434
1443
  const fields = plan.paths;
1435
- const fieldsSignature = [...fields].slice().sort().join(",");
1436
- const argsSignature = [...plan.args.entries()].map(([path, entry]) => `${path}:${entry.hash}`).sort().join(",");
1437
- const groupKey = `${item.type}#${fieldsSignature}|${argsSignature}`;
1444
+ const groupKey = `${type}#${[...fields].slice().sort().join(",")}|${[...plan.args.entries()].map(([path, entry]) => `${path}:${entry.hash}`).sort().join(",")}`;
1438
1445
  let group = groups.get(groupKey);
1439
1446
  if (!group) {
1440
1447
  group = {
1441
1448
  fields,
1442
1449
  ids: [],
1443
1450
  plan,
1444
- type: item.type
1451
+ type
1445
1452
  };
1446
1453
  groups.set(groupKey, group);
1447
1454
  }
1448
1455
  for (const raw of isNode ? [item.id] : item.ids) {
1449
- const entityId = toEntityId(item.type, raw);
1456
+ const entityId = toEntityId(type, raw);
1450
1457
  const missing = this.store.missingForSelection(entityId, fields);
1451
1458
  if (fetchAll || missing.size > 0) group.ids.push(raw);
1452
1459
  }
@@ -1462,10 +1469,11 @@ var FateClient = class {
1462
1469
  hasRequestData(request) {
1463
1470
  for (const [name, item] of Object.entries(request)) {
1464
1471
  const isNode = isNodeItem(item);
1472
+ const type = this.getRootType(name);
1465
1473
  if (isNode || isNodesItem(item)) {
1466
1474
  const fields = getSelectionPlan(item.view, null).paths;
1467
1475
  for (const raw of isNode ? [item.id] : item.ids) {
1468
- const entityId = toEntityId(item.type, raw);
1476
+ const entityId = toEntityId(type, raw);
1469
1477
  if (this.store.missingForSelection(entityId, fields).size > 0) return false;
1470
1478
  }
1471
1479
  continue;
@@ -1483,19 +1491,20 @@ var FateClient = class {
1483
1491
  getRequestResult(request) {
1484
1492
  const result = {};
1485
1493
  for (const [name, item] of Object.entries(request)) {
1494
+ const type = this.getRootType(name);
1486
1495
  if (isNodeItem(item)) {
1487
- result[name] = this.ref(item.type, item.id, item.view);
1496
+ result[name] = this.ref(type, item.id, item.view);
1488
1497
  continue;
1489
1498
  }
1490
1499
  if (isNodesItem(item)) {
1491
- result[name] = item.ids.map((id$1) => this.ref(item.type, id$1, item.view));
1500
+ result[name] = item.ids.map((id$1) => this.ref(type, id$1, item.view));
1492
1501
  continue;
1493
1502
  }
1494
1503
  if (isQueryItem(item)) {
1495
1504
  const entityId = this.rootRequests.get(name);
1496
1505
  if (entityId) {
1497
1506
  const { id: id$1 } = parseEntityId(entityId);
1498
- result[name] = createRef(item.type, id$1, item.view, { root: true });
1507
+ result[name] = createRef(type, id$1, item.view, { root: true });
1499
1508
  } else result[name] = null;
1500
1509
  continue;
1501
1510
  }
@@ -1519,7 +1528,7 @@ var FateClient = class {
1519
1528
  owner: name,
1520
1529
  procedure: `request.${name}`,
1521
1530
  root: true,
1522
- type: item.type
1531
+ type
1523
1532
  };
1524
1533
  Object.defineProperty(connection, ConnectionTag, {
1525
1534
  configurable: false,
@@ -1543,21 +1552,22 @@ var FateClient = class {
1543
1552
  if (!this.transport.fetchQuery) throw new Error(`fate: transport does not support queries. Please add support for 'fetchQuery' in your transport for '${name}'.`);
1544
1553
  const record = await this.transport.fetchQuery(name, plan.paths, argsPayload);
1545
1554
  if (!record || typeof record !== "object") return;
1546
- const entityId = this.writeEntity(item.type, record, plan.paths, void 0, plan);
1555
+ const entityId = this.writeEntity(this.getRootType(name), record, plan.paths, void 0, plan);
1547
1556
  this.rootRequests.set(name, entityId);
1548
1557
  }
1549
1558
  async fetchListAndNormalize(name, item) {
1550
1559
  if (!this.transport.fetchList) throw new Error(`fate: 'transport.fetchList' is not configured but request includes a list for key '${name}'.`);
1560
+ const type = this.getRootType(name);
1551
1561
  const { argsPayload, plan } = this.resolveSelection(item.list, item.args);
1552
1562
  const { items, pagination } = await this.transport.fetchList(name, plan.paths, argsPayload);
1553
1563
  const ids = [];
1554
1564
  const cursors = [];
1555
1565
  for (const entry of items) {
1556
- const id$1 = this.writeEntity(item.type, entry.node, plan.paths, void 0, plan);
1566
+ const id$1 = this.writeEntity(type, entry.node, plan.paths, void 0, plan);
1557
1567
  ids.push(id$1);
1558
1568
  cursors.push(entry.cursor);
1559
1569
  }
1560
- this.registerRootList(item.type, name);
1570
+ this.registerRootList(type, name);
1561
1571
  this.store.setList(name, {
1562
1572
  cursors,
1563
1573
  ids,
@@ -1590,6 +1600,10 @@ var FateClient = class {
1590
1600
  } else if (relationDescriptor && typeof relationDescriptor === "object" && "type" in relationDescriptor) {
1591
1601
  if (isFieldBlocked) continue;
1592
1602
  const childPaths = selectionTree.get(key) ?? emptySet;
1603
+ if (value === null) {
1604
+ result[key] = null;
1605
+ continue;
1606
+ }
1593
1607
  if (value && typeof value === "object" && !isNodeRef(value)) {
1594
1608
  const childType = relationDescriptor.type;
1595
1609
  const childConfig = this.types.get(childType);
@@ -1600,6 +1614,10 @@ var FateClient = class {
1600
1614
  } else if (relationDescriptor && typeof relationDescriptor === "object" && "listOf" in relationDescriptor) {
1601
1615
  if (isFieldBlocked) continue;
1602
1616
  const childPaths = selectionTree.get(key) ?? emptySet;
1617
+ if (value === null) {
1618
+ result[key] = null;
1619
+ continue;
1620
+ }
1603
1621
  const childType = relationDescriptor.listOf;
1604
1622
  const childConfig = this.types.get(childType);
1605
1623
  if (!childConfig) throw new Error(`fate: Unknown related type '${childType}' (field '${type}.${key}').`);
@@ -1745,6 +1763,10 @@ var FateClient = class {
1745
1763
  if (!(key in target)) target[key] = {};
1746
1764
  const nextSelection = Object.keys(selectionWithoutArgs).length ? selectionWithoutArgs : selectionValue;
1747
1765
  const value = record$1[key];
1766
+ if (value == null) {
1767
+ target[key] = null;
1768
+ continue;
1769
+ }
1748
1770
  if (Array.isArray(value)) if (nextSelection.items && typeof nextSelection.items === "object") {
1749
1771
  const selection = nextSelection.items;
1750
1772
  const fieldArgs = plan.args.get(fieldPath);
@@ -1912,4 +1934,16 @@ function createTRPCTransport({ byId, client, lists, mutations, queries }) {
1912
1934
  }
1913
1935
 
1914
1936
  //#endregion
1915
- export { ConnectionTag, FateClient, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
1937
+ //#region src/root.ts
1938
+ /**
1939
+ * Defines a root query for an entity type, capturing the response shape.
1940
+ */
1941
+ function clientRoot(type) {
1942
+ return Object.freeze({
1943
+ [RootKind]: true,
1944
+ type
1945
+ });
1946
+ }
1947
+
1948
+ //#endregion
1949
+ export { ConnectionTag, FateClient, clientRoot, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
package/lib/server.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as AnyRecord } from "./types-BPmcnouE.mjs";
1
+ import { t as AnyRecord } from "./types-CNKGKRtz.mjs";
2
2
  import { z } from "zod";
3
3
  import { TRPCProcedureBuilder } from "@trpc/server";
4
4
 
@@ -164,8 +164,9 @@ type RequestMode = /** (default) Use cached data if present, otherwise fetch. */
164
164
  type RequestOptions = Readonly<{
165
165
  mode?: RequestMode;
166
166
  }>;
167
- type FateClientOptions<Mutations extends FateMutations = EmptyMutations> = {
167
+ type FateClientOptions<Roots extends FateRoots, Mutations extends FateMutations> = {
168
168
  mutations?: Mutations;
169
+ roots: Roots;
169
170
  transport: Transport<MutationMapFromDefinitions<Mutations>>;
170
171
  types: ReadonlyArray<Omit<TypeConfig, 'getId'> & Partial<{
171
172
  getId: TypeConfig['getId'];
@@ -175,7 +176,7 @@ type FateClientOptions<Mutations extends FateMutations = EmptyMutations> = {
175
176
  * Core client that normalizes records, manages the view cache, and coordinates
176
177
  * data fetching.
177
178
  */
178
- declare class FateClient<Mutations extends FateMutations> {
179
+ declare class FateClient<Roots extends FateRoots, Mutations extends FateMutations> {
179
180
  private readonly mutationMap;
180
181
  private readonly parentLists;
181
182
  private readonly rootLists;
@@ -192,7 +193,8 @@ declare class FateClient<Mutations extends FateMutations> {
192
193
  private readonly viewDataCache;
193
194
  readonly actions: MutationActionsFor<Mutations>;
194
195
  readonly mutations: MutationFunctionsFor<Mutations>;
195
- constructor(options: FateClientOptions<Mutations>);
196
+ readonly roots: Roots;
197
+ constructor(options: FateClientOptions<Roots, Mutations>);
196
198
  private initializeParentLists;
197
199
  private registerRootList;
198
200
  private insertIntoRootLists;
@@ -228,12 +230,13 @@ declare class FateClient<Mutations extends FateMutations> {
228
230
  ids: ReadonlyArray<EntityId>;
229
231
  pagination?: Pagination;
230
232
  }> | undefined>;
231
- request<R$1 extends Request>(request: R$1, options?: RequestOptions): Promise<RequestResult<R$1>>;
233
+ request<R$1 extends Request>(request: R$1, options?: RequestOptions): Promise<RequestResult<Roots, R$1>>;
232
234
  releaseRequest(request: Request, mode: RequestMode): void;
233
235
  private handleStoreAndNetworkRequest;
236
+ private getRootType;
234
237
  private executeRequest;
235
238
  private hasRequestData;
236
- getRequestResult<R$1 extends Request>(request: R$1): RequestResult<R$1>;
239
+ getRequestResult<R$1 extends Request>(request: R$1): RequestResult<Roots, R$1>;
237
240
  private fetchByIdAndNormalize;
238
241
  private fetchQueryAndNormalize;
239
242
  private fetchListAndNormalize;
@@ -245,7 +248,7 @@ declare class FateClient<Mutations extends FateMutations> {
245
248
  private pendingPrefix;
246
249
  private pendingKey;
247
250
  }
248
- declare function createClient<Mutations extends FateMutations = EmptyMutations>(options: FateClientOptions<Mutations>): FateClient<Mutations>;
251
+ declare function createClient<T$1 extends [FateRoots, FateMutations] = [Record<never, RootDefinition<any, any>>, Record<never, MutationDefinition<any, any, any>>]>(options: FateClientOptions<T$1[0], T$1[1]>): FateClient<T$1[0], T$1[1]>;
249
252
  //#endregion
250
253
  //#region src/mutation.d.ts
251
254
  /**
@@ -303,7 +306,6 @@ type MutationIdentifierFor<K$1 extends string, Def extends MutationDefinition<an
303
306
  }> : never;
304
307
  type UnionToIntersection<U$1> = (U$1 extends any ? (k: U$1) => void : never) extends ((k: infer I) => void) ? I : never;
305
308
  type NestedValue<Path extends string, Value> = Path extends `${infer Head}.${infer Tail}` ? { [K in Head]: NestedValue<Tail, Value> } : { [K in Path]: Value };
306
- type EmptyMutations = Record<never, MutationDefinition<any, any, any>>;
307
309
  /**
308
310
  * Base type for client mutations.
309
311
  */
@@ -328,6 +330,8 @@ declare const __FateSelectionBrand: unique symbol;
328
330
  declare const __FateMutationEntityBrand: unique symbol;
329
331
  declare const __FateMutationInputBrand: unique symbol;
330
332
  declare const __FateMutationResultBrand: unique symbol;
333
+ declare const __FateRootResultBrand: unique symbol;
334
+ declare const __FateRootTypeBrand: unique symbol;
331
335
  type __ViewEntityAnchor<T$1 extends Entity> = {
332
336
  readonly [__FateEntityBrand]?: T$1;
333
337
  };
@@ -343,12 +347,21 @@ type __MutationInputAnchor<I$1> = {
343
347
  type __MutationResultAnchor<R$1> = {
344
348
  readonly [__FateMutationResultBrand]?: R$1;
345
349
  };
350
+ type __RootResultAnchor<R$1> = {
351
+ readonly [__FateRootResultBrand]?: R$1;
352
+ };
353
+ type __RootTypeAnchor<T$1 extends TypeName> = {
354
+ readonly [__FateRootTypeBrand]?: T$1;
355
+ };
346
356
  /** Unique key that identifies a view composition entry inside a selection or reference. */
347
357
  type ViewTag = `__fate-view__${string}`;
348
358
  /** Determines whether a property key is a fate view tag. */
349
359
  declare function isViewTag(key: string): key is ViewTag;
350
360
  /** Alias for a loose record used throughout the fate's internals. */
351
361
  type AnyRecord = Record<string, unknown>;
362
+ type Nullish<T$1> = Extract<T$1, null | undefined>;
363
+ type NonNullish<T$1> = Exclude<T$1, null | undefined>;
364
+ type WithNullish<T$1, R$1> = R$1 | Nullish<T$1>;
352
365
  type SelectionArgs = Readonly<{
353
366
  args: AnyRecord;
354
367
  }>;
@@ -478,7 +491,8 @@ type EntityName<T$1> = T$1 extends {
478
491
  __typename: infer N extends string;
479
492
  } ? N : never;
480
493
  /** Recursively applies a view selection to an entity to mask fields that aren't selected. */
481
- type Mask<T$1, S> = T$1 extends Array<infer U extends Entity> ? S extends true ? Array<U> : S extends ConnectionSelection<U> ? ConnectionMask<U, S> : HasViewTag<S> extends true ? Array<ViewRef<U['__typename']>> : Array<Mask<U, S>> : S extends true ? T$1 : S extends object ? HasViewTag<S> extends true ? ViewRef<EntityName<NonNullable<T$1>>> : { [K in keyof S as K extends 'args' ? never : K]: S[K] extends true ? NonNullable<T$1>[Extract<K, keyof T$1>] : Mask<NonNullable<T$1>[Extract<K, keyof T$1>], Extract<S[K], object>> } & (T$1 extends Entity ? Pick<NonNullable<T$1>, '__typename'> : Record<never, never>) : T$1;
494
+ type MaskNonNullish<T$1, S> = T$1 extends Array<infer U extends Entity> ? S extends true ? Array<U> : S extends ConnectionSelection<U> ? ConnectionMask<U, S> : HasViewTag<S> extends true ? Array<ViewRef<U['__typename']>> : Array<Mask<U, S>> : S extends true ? T$1 : S extends object ? HasViewTag<S> extends true ? NonNullish<T$1> extends Entity ? ViewRef<EntityName<NonNullish<T$1>>> : ViewRef<EntityName<NonNullable<T$1>>> : { [K in keyof S as K extends 'args' ? never : K]: S[K] extends true ? NonNullish<T$1>[Extract<K, keyof T$1>] : Mask<NonNullish<T$1>[Extract<K, keyof T$1>], Extract<S[K], object>> } & (T$1 extends Entity ? Pick<NonNullish<T$1>, '__typename'> : Record<never, never>) : T$1;
495
+ type Mask<T$1, S> = WithNullish<T$1, MaskNonNullish<NonNullish<T$1>, S>>;
482
496
  /** Entity type captured from a view definition. */
483
497
  type ViewEntity<V> = V extends View<infer T, any> ? T : never;
484
498
  /** Name of the entity type captured from a view definition. */
@@ -491,24 +505,20 @@ type ViewSelection<V> = V extends {
491
505
  type ListItem<V extends View<any, any>> = Readonly<{
492
506
  args?: Record<string, unknown>;
493
507
  list: V;
494
- type: ViewEntityName<V>;
495
508
  }>;
496
509
  /** Definition of a root-level query request. */
497
510
  type QueryItem<V extends View<any, any>> = Readonly<{
498
511
  args?: Record<string, unknown>;
499
- type: ViewEntityName<V>;
500
512
  view: V;
501
513
  }>;
502
514
  /** Definition of a node request with one explicit ID for fetching data from the backend. */
503
515
  type NodeItem<V extends View<any, any>> = Readonly<{
504
516
  id: string | number;
505
- type: ViewEntityName<V>;
506
517
  view: V;
507
518
  }>;
508
519
  /** Definition of a node request with explicit IDs for fetching data from the backend. */
509
520
  type NodesItem<V extends View<any, any>> = Readonly<{
510
521
  ids: ReadonlyArray<string | number>;
511
- type: ViewEntityName<V>;
512
522
  view: V;
513
523
  }>;
514
524
  type RequestItem = ListItem<View<any, any>> | NodeItem<View<any, any>> | NodesItem<View<any, any>> | QueryItem<View<any, any>>;
@@ -530,7 +540,7 @@ type ConnectionNodeType<Root> = Root extends {
530
540
  node?: infer Node;
531
541
  };
532
542
  } ? ViewEntityName<Node & View<any, any>> : never;
533
- type ListResult<Item extends AnyRequestItem> = Item extends AnyNodeItem ? ViewRef<Item['type']> : Item extends AnyNodesItem ? Array<ViewRef<Item['type']>> : Item extends AnyQueryItem ? ViewRef<Item['type']> : Item extends AnyListItem ? Item['list'] extends {
543
+ type ListResult<Item extends AnyRequestItem, Type extends TypeName, Result> = Item extends AnyNodeItem ? ViewRef<Type> : Item extends AnyNodesItem ? Array<ViewRef<Type>> : Item extends AnyQueryItem ? Result extends null ? ViewRef<Type> | null : ViewRef<Type> : Item extends AnyListItem ? Item['list'] extends {
534
544
  items?: {
535
545
  node?: View<any, any>;
536
546
  };
@@ -540,14 +550,27 @@ type ListResult<Item extends AnyRequestItem> = Item extends AnyNodeItem ? ViewRe
540
550
  node: ViewRef<ConnectionNodeType<Item['list']>>;
541
551
  }>;
542
552
  pagination?: Pagination;
543
- }> : Array<ViewRef<Item['type']>> : never;
553
+ }> : Array<ViewRef<Type>> : never;
544
554
  /**
545
555
  * The result of a `FateClient.request` and `useRequest` call, mapping each
546
556
  * request key to its corresponding result.
547
557
  */
548
- type RequestResult<Q extends AnyRequest> = { [K in keyof Q]: ListResult<Q[K]> };
558
+ type RequestResult<R$1 extends FateRoots, Q extends AnyRequest> = { [K in keyof Q]: K extends keyof R$1 ? ListResult<Q[K], RootType<R$1[K]>, RootResult<R$1[K]>> : never };
559
+ /** Brand used on root definitions to mark their identity in the d.ts output. */
560
+ declare const RootKind = "__fate__root";
549
561
  /** Brand used on mutation definitions to mark their identity in the d.ts output. */
550
562
  declare const MutationKind = "__fate__mutation";
563
+ /** Metadata describing a root query for a particular entity and result shape. */
564
+ type RootDefinition<Type extends TypeName, Result> = Readonly<{
565
+ [RootKind]: true;
566
+ type: Type;
567
+ }> & __RootResultAnchor<Result> & __RootTypeAnchor<Type>;
568
+ /** Minimal root description used for typing root maps. */
569
+ type FateRoots = Record<string, RootDefinition<TypeName, unknown>>;
570
+ /** Extracts the entity type name from a root definition. */
571
+ type RootType<R$1> = R$1 extends __RootTypeAnchor<infer T> ? T : never;
572
+ /** Extracts the result type from a root definition. */
573
+ type RootResult<R$1> = R$1 extends __RootResultAnchor<infer Data> ? Data : never;
551
574
  /** Metadata describing a mutation for a particular entity, input, and output. */
552
575
  type MutationDefinition<T$1 extends Entity, I$1, R$1> = Readonly<{
553
576
  entity: T$1['__typename'];
@@ -575,8 +598,6 @@ type MutationMapFromDefinitions<D extends FateMutations> = { [K in keyof D]: {
575
598
  input: MutationInput<D[K]>;
576
599
  output: MutationResult<D[K]>;
577
600
  } };
578
- type Nullish<T$1> = Extract<T$1, null | undefined>;
579
- type NonNullish<T$1> = Exclude<T$1, null | undefined>;
580
601
  type OptimisticUpdateValue<T$1> = T$1 extends ReadonlyArray<infer U> ? Array<OptimisticUpdateValue<U>> : NonNullish<T$1> extends AnyRecord ? OptimisticUpdate<NonNullish<T$1>> | Nullish<T$1> : NonNullish<T$1> | Nullish<T$1>;
581
602
  /** Shape used to describe optimistic updates for mutations. */
582
603
  type OptimisticUpdate<T$1> = { [K in keyof T$1]?: OptimisticUpdateValue<T$1[K]> };
@@ -591,4 +612,4 @@ interface FateThenable<T$1> extends PromiseLike<T$1> {
591
612
  value: T$1;
592
613
  }
593
614
  //#endregion
594
- export { __FateEntityBrand as A, createTRPCTransport as B, ViewData as C, ViewSelection as D, ViewRef as E, FateClient as F, RequestMode as I, RequestOptions as L, isViewTag as M, FateMutations as N, ViewSnapshot as O, mutation as P, createClient as R, View as S, ViewEntityName as T, getSelectionPlan as V, RequestResult as _, Entity as a, TypeConfig as b, Mask as c, MutationIdentifier as d, MutationInput as f, Request as g, Pagination as h, ConnectionTag as i, __FateSelectionBrand as j, ViewTag as k, MutationDefinition as l, NodesItem as m, ConnectionMetadata as n, EntityId as o, MutationResult as p, ConnectionRef as r, ListItem as s, AnyRecord as t, MutationEntity as u, Selection as v, ViewEntity as w, TypeName as x, Snapshot as y, Transport as z };
615
+ export { ViewSelection as A, RequestOptions as B, TypeConfig as C, ViewEntity as D, ViewData as E, isViewTag as F, Transport as H, FateMutations as I, mutation as L, ViewTag as M, __FateEntityBrand as N, ViewEntityName as O, __FateSelectionBrand as P, FateClient as R, Snapshot as S, View as T, createTRPCTransport as U, createClient as V, getSelectionPlan as W, Pagination as _, Entity as a, RootDefinition as b, FateThenable as c, MutationDefinition as d, MutationEntity as f, NodesItem as g, MutationResult as h, ConnectionTag as i, ViewSnapshot as j, ViewRef as k, ListItem as l, MutationInput as m, ConnectionMetadata as n, EntityId as o, MutationIdentifier as p, ConnectionRef as r, FateRoots as s, AnyRecord as t, Mask as u, Request as v, TypeName as w, Selection as x, RequestResult as y, RequestMode as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/fate",
3
- "version": "0.0.8",
3
+ "version": "0.1.0",
4
4
  "description": "fate is a modern data client for React.",
5
5
  "homepage": "https://github.com/nkzw-tech/fate",
6
6
  "repository": {