@nkzw/fate 0.0.6 → 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 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
- _[Learn more](/docs/guide/getting-started.md) about fate's core concepts and features._
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
- The `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.
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,7 +549,11 @@ 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 } }) => {
556
+ const fate = useFateClient();
529
557
  const [result, like] = useActionState(fate.actions.post.like, null);
530
558
 
531
559
  return (
@@ -540,6 +568,7 @@ If you are not using an async component library, you can use React's `useTransit
540
568
 
541
569
  ```tsx
542
570
  const LikeButton = ({ post }: { post: { id: string; likes: number } }) => {
571
+ const fate = useFateClient();
543
572
  const [, startTransition] = useTransition();
544
573
  const [result, like, isPending] = useActionState(
545
574
  fate.actions.post.like,
@@ -661,16 +690,17 @@ export const postRouter = router({
661
690
  view: postDataView,
662
691
  });
663
692
 
664
- const updated = await ctx.prisma.post.update({
665
- data: {
666
- likes: {
667
- increment: 1,
693
+ return resolve(
694
+ await ctx.prisma.post.update({
695
+ data: {
696
+ likes: {
697
+ increment: 1,
698
+ },
668
699
  },
669
- },
670
- select,
671
- where: { id: input.id },
672
- });
673
- return resolve(updated as unknown as PostItem);
700
+ select,
701
+ where: { id: input.id },
702
+ } as PostUpdateArgs),
703
+ );
674
704
  }),
675
705
  });
676
706
  ```
@@ -739,6 +769,26 @@ useEffect(() => {
739
769
  }, [like, result]);
740
770
  ```
741
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
+
742
792
  ## Server Integration
743
793
 
744
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.
@@ -785,35 +835,27 @@ _Note: Currently, fate provides helpers to integrate with Prisma, but the framew
785
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`:
786
836
 
787
837
  ```tsx
788
- import { connectionArgs, createResolver } from '@nkzw/fate/server';
838
+ import { byIdInput, createResolver } from '@nkzw/fate/server';
789
839
  import { z } from 'zod';
790
840
  import type { UserFindManyArgs } from '../../prisma/prisma-client/models.ts';
791
841
  import { procedure, router } from '../init.ts';
792
842
  import { userDataView } from '../views.ts';
793
843
 
794
844
  export const userRouter = router({
795
- byId: procedure
796
- .input(
797
- z.object({
798
- args: connectionArgs,
799
- ids: z.array(z.string().min(1)).nonempty(),
800
- select: z.array(z.string()),
801
- }),
802
- )
803
- .query(async ({ ctx, input }) => {
804
- const { resolveMany, select } = createResolver({
805
- ...input,
806
- ctx,
807
- view: userDataView,
808
- });
809
-
810
- const users = await ctx.prisma.user.findMany({
811
- select: select,
812
- where: { id: { in: input.ids } },
813
- } as UserFindManyArgs);
814
-
815
- return await resolveMany(users);
816
- }),
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
+ }),
817
859
  });
818
860
  ```
819
861
 
@@ -894,29 +936,19 @@ export const postDataView = dataView<PostItem>('Post')({
894
936
  });
895
937
  ```
896
938
 
897
- We can also define root-level lists by exporting a `Lists` object from our `views.ts` file:
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:
898
940
 
899
941
  ```tsx
900
- export const Lists = {
901
- posts: postDataView,
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,
902
948
  };
903
949
  ```
904
950
 
905
- This makes it possible to fetch a list of posts from the client using `useRequest`.
906
-
907
- #### Custom Root Lists
908
-
909
- 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:
910
-
911
- ```tsx
912
- export const Lists = {
913
- // …
914
- postSearch: { procedure: 'search', view: postDataView },
915
- // …
916
- };
917
- ```
918
-
919
- 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.
920
952
 
921
953
  ### Data View Resolvers
922
954
 
@@ -925,19 +957,33 @@ fate data views support resolvers for computed fields. If we want to add a `comm
925
957
  ```tsx
926
958
  export const postDataView = dataView<PostItem>('Post')({
927
959
  author: userDataView,
928
- commentCount: resolver<PostItem>({
929
- resolve: ({ item }) => item._count?.comments ?? 0,
960
+ commentCount: resolver<PostItem, number>({
961
+ resolve: ({ _count }) => _count?.comments ?? 0,
930
962
  select: () => ({
931
963
  _count: { select: { comments: true } },
932
964
  }),
933
965
  }),
934
966
  comments: list(commentDataView),
935
967
  id: true,
936
- } as const;
968
+ });
937
969
  ```
938
970
 
939
971
  This definition makes the `commentCount` field available to your client-side views.
940
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
+
941
987
  ### Generating a typed client
942
988
 
943
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.
@@ -959,10 +1005,10 @@ export type AppRouter = typeof appRouter;
959
1005
  export * from './views.ts';
960
1006
  ```
961
1007
 
962
- _Note: We try to keep magic to a minimum and you can handwrite the [generated client](https://github.com/nkzw-tech/fate/blob/main/example/client/src/lib/fate.generated.ts) if you prefer._
1008
+ _Note: We try to keep magic to a minimum and you can handwrite the [generated client](https://github.com/nkzw-tech/fate/blob/main/example/client/src/fate.ts) if you prefer._
963
1009
 
964
1010
  ```bash
965
- pnpm fate generate @your-org/server/trpc/router.ts client/src/lib/fate.generated.ts
1011
+ pnpm fate generate @your-org/server/trpc/router.ts client/src/fate.ts
966
1012
  ```
967
1013
 
968
1014
  _Note: fate uses the specified server module name to extract the server types it needs and uses the same module name to import the views into the generated client. Make sure that the module is available both at the root where you are running the CLI and in the client package._
@@ -1024,7 +1070,9 @@ Probably. One day. _Maybe._
1024
1070
  ### How was fate built?
1025
1071
 
1026
1072
  > [!NOTE]
1027
- > 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.
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).
1028
1076
 
1029
1077
  ## Future
1030
1078
 
@@ -1041,5 +1089,6 @@ Probably. One day. _Maybe._
1041
1089
 
1042
1090
  - [Relay](https://relay.dev/), [Isograph](https://isograph.dev/) & [GraphQL](https://graphql.org/) for inspiration
1043
1091
  - [Ricky Hanlon](https://x.com/rickyfm) for guidance on Async React
1092
+ - [Anthony Powell](https://x.com/Cephalization) for testing fate and providing feedback
1044
1093
 
1045
- **_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
- * list resolver configs.
10
+ * root resolver configs.
11
11
  */
12
- const createSchema = (dataViews, lists) => {
12
+ const createSchema = (dataViews, roots) => {
13
13
  const canonicalViews = /* @__PURE__ */ new Map();
14
- const entities = {};
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 [name, list] of Object.entries(lists)) {
42
- const config = "fields" in list ? { view: list } : list;
43
- const typeName = ensureType(config.view);
44
- entities[typeName.toLowerCase()] = {
45
- list: name,
46
- listProcedure: config.procedure,
47
- type: typeName
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
- entities,
57
+ roots: rootSchema,
53
58
  types: Array.from(fateTypes.values())
54
59
  };
55
60
  };
@@ -66,7 +71,7 @@ Generates the fate client from the server's tRPC router.
66
71
  ${styleText("dim", "<moduleName>")} The module name to import the tRPC router from.
67
72
  ${styleText("dim", "<targetFile>")} The file path to write the generated client to.
68
73
 
69
- ${styleText("bold", "Example:")} ${styleText("blue", `pnpm fate generate @org/server/trpc/router.ts client/lib/fate.generated.ts`)}
74
+ ${styleText("bold", "Example:")} ${styleText("blue", `pnpm fate generate @org/server/trpc/router.ts client/lib/fate.ts`)}
70
75
  `);
71
76
  process.exit(1);
72
77
  }
@@ -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, Lists, ...dataViews } = await import(moduleName);
93
- const { entities, types } = createSchema(Object.values(dataViews), Lists);
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 entity = entities[router];
100
- if (!entity) continue;
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: entity.type,
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: entity.type,
128
+ entityType,
116
129
  router
117
130
  });
118
131
  continue;
119
132
  }
120
- const listProcedure = entity.listProcedure ?? "list";
121
- if (procedureName === listProcedure && type === "query" && entity.list) listEntries.push({
122
- list: entity.list,
123
- procedure: listProcedure,
124
- router
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
- const mutationConfigBlock = indentBlock(mutationConfigLines.join("\n"), 6);
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';
@@ -162,27 +189,35 @@ type TRPCClientType = ReturnType<typeof createTRPCProxyClient<AppRouter>>;
162
189
  type RouterInputs = inferRouterInputs<AppRouter>;
163
190
  type RouterOutputs = inferRouterOutputs<AppRouter>;
164
191
 
192
+ const mutations = {
193
+ ${mutationConfigBlock}
194
+ } as const;
195
+
196
+ type GeneratedClientMutations = typeof mutations;
197
+
198
+ declare module 'react-fate' {
199
+ interface ClientMutations extends GeneratedClientMutations {}
200
+ }
201
+
165
202
  export const createFateClient = (options: {
166
203
  links: Parameters<typeof createTRPCProxyClient>[0]['links'];
167
204
  }) => {
168
205
  const trpcClient = createTRPCProxyClient<AppRouter>(options);
169
206
 
170
- const mutations = {
207
+ const trpcMutations = {
171
208
  ${mutationResolverBlock}
172
209
  } as const;
173
210
 
174
211
  return createClient({
175
- mutations: {
176
- ${mutationConfigBlock}
177
- },
178
- transport: createTRPCTransport<AppRouter, typeof mutations>({
212
+ mutations,
213
+ transport: createTRPCTransport<AppRouter, typeof trpcMutations>({
179
214
  byId: {
180
215
  ${byIdBlock}
181
216
  },
182
217
  client: trpcClient,
183
- ${listLines.length ? ` lists: {\n${indentBlock(listsBlockContent, 8)}\n },\n` : ""} mutations,
218
+ ${queryLines.length ? ` queries: {\n${indentBlock(queriesBlockContent, 8)}\n },\n` : ""}${listsBlock} mutations: trpcMutations,
184
219
  }),
185
- types: ${typesBlock},
220
+ types: ${typesBlock.trimStart()},
186
221
  });
187
222
  };
188
223
  `;