@nkzw/fate 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -60
- package/lib/cli.mjs +52 -25
- package/lib/index.d.mts +5 -569
- package/lib/index.mjs +102 -29
- package/lib/server.d.mts +35 -29
- package/lib/server.mjs +19 -9
- package/lib/types-BPmcnouE.d.mts +594 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,21 +26,42 @@
|
|
|
26
26
|
|
|
27
27
|
GraphQL and Relay introduced several novel ideas: fragments co‑located with components, [a normalized cache](https://relay.dev/docs/principles-and-architecture/thinking-in-graphql/#caching-a-graph) keyed by global identifiers, and a compiler that hoists fragments into a single network request. These innovations made it possible to build large applications where data requirements are modular and self‑contained.
|
|
28
28
|
|
|
29
|
-
Nakazawa Tech builds apps primarily with GraphQL and Relay. We advocate for these technologies in [talks](https://www.youtube.com/watch?v=rxPTEko8J7c&t=36s) and provide templates ([server](https://github.com/nkzw-tech/server-template), [client](https://github.com/nkzw-tech/web-app-template/tree/with-relay)) to help developers get started quickly.
|
|
29
|
+
[Nakazawa Tech](https://nakazawa.tech) builds apps and [games](https://athenacrisis.com) primarily with GraphQL and Relay. We advocate for these technologies in [talks](https://www.youtube.com/watch?v=rxPTEko8J7c&t=36s) and provide templates ([server](https://github.com/nkzw-tech/server-template), [client](https://github.com/nkzw-tech/web-app-template/tree/with-relay)) to help developers get started quickly.
|
|
30
30
|
|
|
31
31
|
However, GraphQL comes with its own type system and query language. If you are already using tRPC or another type‑safe RPC framework, it's a significant investment to adopt and implement GraphQL on the backend. This investment often prevents teams from adopting Relay on the frontend.
|
|
32
32
|
|
|
33
33
|
Many React data frameworks lack Relay's ergonomics, especially fragment composition, co-located data requirements, predictable caching, and deep integration with modern React features. Optimistic updates usually require manually managing keys and imperative data updates, which is error-prone and tedious.
|
|
34
34
|
|
|
35
|
-
fate takes the great ideas from Relay and puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching.
|
|
35
|
+
fate takes the great ideas from Relay and puts them on top of tRPC. You get the best of both worlds: type safety between the client and server, and GraphQL-like ergonomics for data fetching. Using _fate_ usually looks like this:
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
```tsx
|
|
38
|
+
export const PostView = view<Post>()({
|
|
39
|
+
content: true,
|
|
40
|
+
id: true,
|
|
41
|
+
title: true,
|
|
42
|
+
author: UserView,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const PostCard = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
46
|
+
const post = useView(PostView, postRef);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Card>
|
|
50
|
+
<h2>{post.title}</h2>
|
|
51
|
+
<p>{post.content}</p>
|
|
52
|
+
<UserCard user={post.author} />
|
|
53
|
+
</Card>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
_[Learn more](/docs/guide/getting-started.md) about fate's core concepts or get started with [a ready-made template](https://github.com/nkzw-tech/fate-template#readme)._
|
|
38
59
|
|
|
39
60
|
## Getting Started
|
|
40
61
|
|
|
41
62
|
### Template
|
|
42
63
|
|
|
43
|
-
Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template) quickly:
|
|
64
|
+
Get started with [a ready-made template](https://github.com/nkzw-tech/fate-template#readme) quickly:
|
|
44
65
|
|
|
45
66
|
::: code-group
|
|
46
67
|
|
|
@@ -58,7 +79,7 @@ yarn dlx giget@latest gh:nkzw-tech/fate-template
|
|
|
58
79
|
|
|
59
80
|
:::
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
`fate-template` comes with a simple tRPC backend and a React frontend using **_fate_**. It features modern tools to deliver an incredibly fast development experience. Follow its [README.md](https://github.com/nkzw-tech/fate-template#fate-quick-start-template) to get started.
|
|
62
83
|
|
|
63
84
|
### Manual Installation
|
|
64
85
|
|
|
@@ -126,6 +147,9 @@ Traditionally, React apps are built with components and hooks. fate introduces a
|
|
|
126
147
|
|
|
127
148
|
With fate, you no longer worry about _when_ to fetch data, how to coordinate loading states, or how to handle errors imperatively. You avoid overfetching, stop passing unnecessary data down the tree, and eliminate boilerplate types created solely for passing server data to child components.
|
|
128
149
|
|
|
150
|
+
> [!NOTE]
|
|
151
|
+
> Views in _fate_ are what fragments are in GraphQL.
|
|
152
|
+
|
|
129
153
|
## Views
|
|
130
154
|
|
|
131
155
|
### Defining Views
|
|
@@ -525,6 +549,9 @@ Mutations in your tRPC backend are made available as actions and mutations by fa
|
|
|
525
549
|
Let's assume that our `Post` entity has a tRPC mutation for liking a post called `post.like`. A `LikeButton` component using fate Actions and an async component library could then look like this:
|
|
526
550
|
|
|
527
551
|
```tsx
|
|
552
|
+
import { useActionState } from 'react';
|
|
553
|
+
import { useFateClient } from 'react-fate';
|
|
554
|
+
|
|
528
555
|
const LikeButton = ({ post }: { post: { id: string; likes: number } }) => {
|
|
529
556
|
const fate = useFateClient();
|
|
530
557
|
const [result, like] = useActionState(fate.actions.post.like, null);
|
|
@@ -663,16 +690,17 @@ export const postRouter = router({
|
|
|
663
690
|
view: postDataView,
|
|
664
691
|
});
|
|
665
692
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
693
|
+
return resolve(
|
|
694
|
+
await ctx.prisma.post.update({
|
|
695
|
+
data: {
|
|
696
|
+
likes: {
|
|
697
|
+
increment: 1,
|
|
698
|
+
},
|
|
670
699
|
},
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
return resolve(updated as unknown as PostItem);
|
|
700
|
+
select,
|
|
701
|
+
where: { id: input.id },
|
|
702
|
+
} as PostUpdateArgs),
|
|
703
|
+
);
|
|
676
704
|
}),
|
|
677
705
|
});
|
|
678
706
|
```
|
|
@@ -741,6 +769,26 @@ useEffect(() => {
|
|
|
741
769
|
}, [like, result]);
|
|
742
770
|
```
|
|
743
771
|
|
|
772
|
+
### Controlling List Insertion Behavior
|
|
773
|
+
|
|
774
|
+
When inserting new objects into lists, the default behavior is to append the new object to the list. You can provide an `insert` option with `before`, `after` or `none` values to customize this behavior and specify where the new object should be inserted in the list:
|
|
775
|
+
|
|
776
|
+
```tsx
|
|
777
|
+
addComment({
|
|
778
|
+
input: { content: 'New Comment text', postId: post.id },
|
|
779
|
+
insert: 'before', // Insert the new comment at the beginning of the list.
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
Or, use the `none` option if you want to ignore inserting the new object into any lists:
|
|
784
|
+
|
|
785
|
+
```tsx
|
|
786
|
+
addComment({
|
|
787
|
+
input: { content: 'New Comment text', postId: post.id },
|
|
788
|
+
insert: 'none', // Do not insert the new comment into any lists.
|
|
789
|
+
});
|
|
790
|
+
```
|
|
791
|
+
|
|
744
792
|
## Server Integration
|
|
745
793
|
|
|
746
794
|
Until now, we have focused on the client-side API of fate. You'll need a tRPC backend that follows some conventions so you can generate a typed client using fate's CLI. At the moment _fate_ is designed to work with tRPC and Prisma, but the framework is not coupled to any particular ORM or database, it's just what we are starting with.
|
|
@@ -787,35 +835,27 @@ _Note: Currently, fate provides helpers to integrate with Prisma, but the framew
|
|
|
787
835
|
We can apply the above data view in our tRPC router and resolve the client's selection against it using `createResolver`. Here is an example implementation of the `byId` query for the `User` type which allows fetching multiple users by `id`:
|
|
788
836
|
|
|
789
837
|
```tsx
|
|
790
|
-
import {
|
|
838
|
+
import { byIdInput, createResolver } from '@nkzw/fate/server';
|
|
791
839
|
import { z } from 'zod';
|
|
792
840
|
import type { UserFindManyArgs } from '../../prisma/prisma-client/models.ts';
|
|
793
841
|
import { procedure, router } from '../init.ts';
|
|
794
842
|
import { userDataView } from '../views.ts';
|
|
795
843
|
|
|
796
844
|
export const userRouter = router({
|
|
797
|
-
byId: procedure
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const users = await ctx.prisma.user.findMany({
|
|
813
|
-
select: select,
|
|
814
|
-
where: { id: { in: input.ids } },
|
|
815
|
-
} as UserFindManyArgs);
|
|
816
|
-
|
|
817
|
-
return await resolveMany(users);
|
|
818
|
-
}),
|
|
845
|
+
byId: procedure.input(byIdInput).query(async ({ ctx, input }) => {
|
|
846
|
+
const { resolveMany, select } = createResolver({
|
|
847
|
+
...input,
|
|
848
|
+
ctx,
|
|
849
|
+
view: userDataView,
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
const users = await ctx.prisma.user.findMany({
|
|
853
|
+
select: select,
|
|
854
|
+
where: { id: { in: input.ids } },
|
|
855
|
+
} as UserFindManyArgs);
|
|
856
|
+
|
|
857
|
+
return await resolveMany(users);
|
|
858
|
+
}),
|
|
819
859
|
});
|
|
820
860
|
```
|
|
821
861
|
|
|
@@ -896,29 +936,19 @@ export const postDataView = dataView<PostItem>('Post')({
|
|
|
896
936
|
});
|
|
897
937
|
```
|
|
898
938
|
|
|
899
|
-
We can
|
|
939
|
+
We can define extra root-level lists and queries by exporting a `Root` object from our `views.ts` file using the same view syntax as everywhere else:
|
|
900
940
|
|
|
901
941
|
```tsx
|
|
902
|
-
export const
|
|
903
|
-
|
|
942
|
+
export const Root = {
|
|
943
|
+
categories: list(categoryDataView),
|
|
944
|
+
commentSearch: { procedure: 'search', view: list(commentDataView) },
|
|
945
|
+
events: list(eventDataView),
|
|
946
|
+
posts: list(postDataView),
|
|
947
|
+
viewer: userDataView,
|
|
904
948
|
};
|
|
905
949
|
```
|
|
906
950
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
#### Custom Root Lists
|
|
910
|
-
|
|
911
|
-
You might want to define custom root lists that don't directly map to a single data view. For example, a search endpoint that returns a list of posts based on a search query:
|
|
912
|
-
|
|
913
|
-
```tsx
|
|
914
|
-
export const Lists = {
|
|
915
|
-
// …
|
|
916
|
-
postSearch: { procedure: 'search', view: postDataView },
|
|
917
|
-
// …
|
|
918
|
-
};
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
This maps the `postSearch` list to a `search` procedure on your post router.
|
|
951
|
+
Entries that wrap their view in `list(...)` are treated as list resolvers and use the `procedure` name when calling the corresponding router procedure, defaulting to `list`. If you omit `list(...)`, fate treats the entry as a standard query and uses the view type name to infer the router name.
|
|
922
952
|
|
|
923
953
|
### Data View Resolvers
|
|
924
954
|
|
|
@@ -927,19 +957,33 @@ fate data views support resolvers for computed fields. If we want to add a `comm
|
|
|
927
957
|
```tsx
|
|
928
958
|
export const postDataView = dataView<PostItem>('Post')({
|
|
929
959
|
author: userDataView,
|
|
930
|
-
commentCount: resolver<PostItem>({
|
|
931
|
-
resolve: ({
|
|
960
|
+
commentCount: resolver<PostItem, number>({
|
|
961
|
+
resolve: ({ _count }) => _count?.comments ?? 0,
|
|
932
962
|
select: () => ({
|
|
933
963
|
_count: { select: { comments: true } },
|
|
934
964
|
}),
|
|
935
965
|
}),
|
|
936
966
|
comments: list(commentDataView),
|
|
937
967
|
id: true,
|
|
938
|
-
}
|
|
968
|
+
});
|
|
939
969
|
```
|
|
940
970
|
|
|
941
971
|
This definition makes the `commentCount` field available to your client-side views.
|
|
942
972
|
|
|
973
|
+
### Authorization in Resolvers
|
|
974
|
+
|
|
975
|
+
You might want to restrict access to certain fields based on the current user or other contextual information. You can do this by adding an `authorize` function to your resolver definition:
|
|
976
|
+
|
|
977
|
+
```tsx
|
|
978
|
+
export const userDataView = dataView<UserItem>('User')({
|
|
979
|
+
email: resolver<UserItem, string | null, { sessionUser: string }>({
|
|
980
|
+
authorize: ({ id }, context) => context?.sessionUserId === id,
|
|
981
|
+
resolve: ({ email }) => email,
|
|
982
|
+
}),
|
|
983
|
+
id: true,
|
|
984
|
+
});
|
|
985
|
+
```
|
|
986
|
+
|
|
943
987
|
### Generating a typed client
|
|
944
988
|
|
|
945
989
|
Now that we have defined our client views and our tRPC server, we need to connect them with some glue code. We recommend using fate's CLI for convenience.
|
|
@@ -1026,7 +1070,9 @@ Probably. One day. _Maybe._
|
|
|
1026
1070
|
### How was fate built?
|
|
1027
1071
|
|
|
1028
1072
|
> [!NOTE]
|
|
1029
|
-
> 80% of _fate_'s code was written by OpenAI's Codex – four versions per task, carefully curated by a human. The remaining 20% was written by [@cnakazawa](https://x.com/cnakazawa).
|
|
1073
|
+
> 80% of _fate_'s code was written by OpenAI's Codex – four versions per task, carefully curated by a human. The remaining 20% was written by [@cnakazawa](https://x.com/cnakazawa). _You get to decide which parts are the good ones!_ The docs were 100% written by a human.
|
|
1074
|
+
>
|
|
1075
|
+
> If you contribute to _fate_, we [require you to disclose your use of AI tools](https://github.com/nkzw-tech/fate/blob/main/CONTRIBUTING.md#ai-assistance-notice).
|
|
1030
1076
|
|
|
1031
1077
|
## Future
|
|
1032
1078
|
|
|
@@ -1045,4 +1091,4 @@ Probably. One day. _Maybe._
|
|
|
1045
1091
|
- [Ricky Hanlon](https://x.com/rickyfm) for guidance on Async React
|
|
1046
1092
|
- [Anthony Powell](https://x.com/Cephalization) for testing fate and providing feedback
|
|
1047
1093
|
|
|
1048
|
-
**_fate_** was created by [@cnakazawa](https://x.com/cnakazawa) and is maintained by [Nakazawa Tech](https://nakazawa.tech/).
|
|
1094
|
+
**_fate_** was created by [@cnakazawa](https://x.com/cnakazawa) and is maintained by [Nakazawa Tech](https://nakazawa.tech/).
|
package/lib/cli.mjs
CHANGED
|
@@ -7,11 +7,11 @@ import { styleText } from "node:util";
|
|
|
7
7
|
const isDataViewField = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
|
|
8
8
|
/**
|
|
9
9
|
* Builds the schema object used by the CLI generator from your data views and
|
|
10
|
-
*
|
|
10
|
+
* root resolver configs.
|
|
11
11
|
*/
|
|
12
|
-
const createSchema = (dataViews,
|
|
12
|
+
const createSchema = (dataViews, roots) => {
|
|
13
13
|
const canonicalViews = /* @__PURE__ */ new Map();
|
|
14
|
-
const
|
|
14
|
+
const rootSchema = {};
|
|
15
15
|
const fateTypes = /* @__PURE__ */ new Map();
|
|
16
16
|
const processing = /* @__PURE__ */ new Set();
|
|
17
17
|
const ensureType = (view) => {
|
|
@@ -36,20 +36,25 @@ const createSchema = (dataViews, lists) => {
|
|
|
36
36
|
const typeName = view.typeName;
|
|
37
37
|
if (!typeName) throw new Error("Data view is missing a type name.");
|
|
38
38
|
if (!canonicalViews.has(typeName)) canonicalViews.set(typeName, view);
|
|
39
|
-
entities[typeName.toLowerCase()] = { type: typeName };
|
|
40
39
|
}
|
|
41
|
-
for (const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
for (const view of dataViews) ensureType(view);
|
|
41
|
+
for (const [name, root$1] of Object.entries(roots)) {
|
|
42
|
+
const config = "fields" in root$1 ? { view: root$1 } : root$1;
|
|
43
|
+
const view = config.view;
|
|
44
|
+
const type = ensureType(view);
|
|
45
|
+
if (!view.typeName) throw new Error(`Root "${name}" is missing a data view.`);
|
|
46
|
+
const router = config.router ?? view.typeName[0]?.toLowerCase() + view.typeName.slice(1);
|
|
47
|
+
if (!router) throw new Error(`Root "${name}" is missing a router name.`);
|
|
48
|
+
rootSchema[name] = {
|
|
49
|
+
kind: view.kind === "list" ? "list" : "query",
|
|
50
|
+
procedure: config.procedure ?? (view.kind === "list" ? "list" : name),
|
|
51
|
+
router,
|
|
52
|
+
type
|
|
48
53
|
};
|
|
49
54
|
}
|
|
50
55
|
for (const view of dataViews) ensureType(view);
|
|
51
56
|
return {
|
|
52
|
-
|
|
57
|
+
roots: rootSchema,
|
|
53
58
|
types: Array.from(fateTypes.values())
|
|
54
59
|
};
|
|
55
60
|
};
|
|
@@ -89,21 +94,29 @@ const formatTypes = (types) => {
|
|
|
89
94
|
const indentBlock = (value, spaces) => value.split("\n").map((line) => line.length ? `${" ".repeat(spaces)}${line}` : line).join("\n");
|
|
90
95
|
const generate = async () => {
|
|
91
96
|
console.log(styleText("bold", `Generating fate client…\n`));
|
|
92
|
-
const { appRouter,
|
|
93
|
-
const {
|
|
97
|
+
const { appRouter, Root, ...dataViews } = await import(moduleName);
|
|
98
|
+
const { roots, types } = createSchema(Object.values(dataViews), Root ?? {});
|
|
94
99
|
const routerRecord = appRouter._def?.record ?? {};
|
|
95
100
|
const mutationEntries = [];
|
|
96
101
|
const byIdEntries = [];
|
|
97
102
|
const listEntries = [];
|
|
103
|
+
const queryEntries = [];
|
|
104
|
+
const rootsByRouter = /* @__PURE__ */ new Map();
|
|
105
|
+
for (const entry of Object.entries(roots)) {
|
|
106
|
+
const list = rootsByRouter.get(entry[1].router) ?? [];
|
|
107
|
+
list.push(entry);
|
|
108
|
+
rootsByRouter.set(entry[1].router, list);
|
|
109
|
+
}
|
|
98
110
|
for (const [router, procedures] of Object.entries(routerRecord)) {
|
|
99
|
-
const
|
|
100
|
-
if (!
|
|
111
|
+
const routerRoots = rootsByRouter.get(router);
|
|
112
|
+
if (!routerRoots?.length) continue;
|
|
113
|
+
const entityType = routerRoots[0][1].type;
|
|
101
114
|
for (const [procedureName, procedure] of Object.entries(procedures)) {
|
|
102
115
|
const type = procedure?._def?.type;
|
|
103
116
|
if (!type) continue;
|
|
104
117
|
if (type === "mutation") {
|
|
105
118
|
mutationEntries.push({
|
|
106
|
-
entityType
|
|
119
|
+
entityType,
|
|
107
120
|
name: `${router}.${procedureName}`,
|
|
108
121
|
procedure: procedureName,
|
|
109
122
|
router
|
|
@@ -112,21 +125,32 @@ const generate = async () => {
|
|
|
112
125
|
}
|
|
113
126
|
if (procedureName === "byId" && type === "query") {
|
|
114
127
|
byIdEntries.push({
|
|
115
|
-
entityType
|
|
128
|
+
entityType,
|
|
116
129
|
router
|
|
117
130
|
});
|
|
118
131
|
continue;
|
|
119
132
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
for (const [queryName, root$1] of routerRoots) {
|
|
134
|
+
if (root$1.kind !== "query") continue;
|
|
135
|
+
if (procedureName === root$1.procedure && type === "query") queryEntries.push({
|
|
136
|
+
name: queryName,
|
|
137
|
+
procedure: root$1.procedure,
|
|
138
|
+
router
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const [listName, root$1] of routerRoots) {
|
|
142
|
+
if (root$1.kind !== "list") continue;
|
|
143
|
+
if (procedureName === root$1.procedure && type === "query") listEntries.push({
|
|
144
|
+
list: listName,
|
|
145
|
+
procedure: root$1.procedure,
|
|
146
|
+
router
|
|
147
|
+
});
|
|
148
|
+
}
|
|
126
149
|
}
|
|
127
150
|
}
|
|
128
151
|
mutationEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
129
152
|
byIdEntries.sort((a, b) => a.entityType.localeCompare(b.entityType));
|
|
153
|
+
queryEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
130
154
|
listEntries.sort((a, b) => a.list.localeCompare(b.list));
|
|
131
155
|
const viewTypes = Array.from(["AppRouter", ...new Set(mutationEntries.map((entry) => entry.entityType))]).sort();
|
|
132
156
|
const mutationResolverLines = mutationEntries.map(({ name, procedure, router }) => `'${name}': (client: TRPCClientType) => client.${router}.${procedure}.mutate,`);
|
|
@@ -146,12 +170,15 @@ const generate = async () => {
|
|
|
146
170
|
select,
|
|
147
171
|
}),`);
|
|
148
172
|
const listLines = listEntries.map(({ list, procedure, router }) => `${list}: (client: TRPCClientType) => client.${router}.${procedure}.query,`);
|
|
173
|
+
const queryLines = queryEntries.map(({ name, procedure, router }) => `${name}: (client: TRPCClientType) => client.${router}.${procedure}.query,`);
|
|
149
174
|
const typeImports = `import type { ${viewTypes.join(", ")} } from '${moduleName}';`;
|
|
150
175
|
const typesBlock = indentBlock(formatTypes(types), 6);
|
|
151
176
|
const mutationResolverBlock = indentBlock(mutationResolverLines.join("\n"), 4);
|
|
152
177
|
const mutationConfigBlock = indentBlock(mutationConfigLines.join("\n"), 2);
|
|
153
178
|
const byIdBlock = indentBlock(byIdLines.join("\n"), 8);
|
|
154
179
|
const listsBlockContent = listLines.join("\n");
|
|
180
|
+
const listsBlock = listLines.length ? ` lists: {\n${indentBlock(listsBlockContent, 8)}\n },\n` : "";
|
|
181
|
+
const queriesBlockContent = queryLines.join("\n");
|
|
155
182
|
const source = `// @generated by \`pnpm fate generate\`
|
|
156
183
|
${typeImports}
|
|
157
184
|
import { createTRPCProxyClient } from '@trpc/client';
|
|
@@ -188,7 +215,7 @@ ${mutationResolverBlock}
|
|
|
188
215
|
${byIdBlock}
|
|
189
216
|
},
|
|
190
217
|
client: trpcClient,
|
|
191
|
-
${
|
|
218
|
+
${queryLines.length ? ` queries: {\n${indentBlock(queriesBlockContent, 8)}\n },\n` : ""}${listsBlock} mutations: trpcMutations,
|
|
192
219
|
}),
|
|
193
220
|
types: ${typesBlock.trimStart()},
|
|
194
221
|
});
|