@zachacious/protoc-gen-connect-vue 1.0.5 → 1.0.7

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
@@ -1,210 +1,175 @@
1
- # **@zachacious/protoc-gen-connect-vue**
1
+ # **protoc-gen-connect-vue**
2
2
 
3
- A specialized protoc plugin designed to generate a production-grade, reactive TypeScript SDK for **Vue.js**. This plugin serves as an intelligent orchestration layer on top of [ConnectRPC](https://connectrpc.com/) and [TanStack Vue Query](https://tanstack.com/query/latest), automating the tedious aspects of state management, cache synchronization, and pagination.
3
+ protoc-gen-connect-vue is a code generation plugin for [ConnectRPC](https://connectrpc.com/) tailored specifically for Vue.js. It generates a type-safe SDK that combines the power of ConnectRPC with the reactivity of the Vue Composition API and the caching capabilities of TanStack Query (Vue).
4
4
 
5
- ## **🏗 Context & Architecture**
5
+ Unlike the official @connectrpc/connect-query which is architected for React, this plugin is built from the ground up for Vue developers, providing first-class support for ref, computed, and manual resource invalidation.
6
6
 
7
- This project is not a replacement for standard tooling but an enhancement of it. It leverages the official [connect-query-es](https://github.com/connectrpc/connect-query-es) plugin to generate underlying query definitions and wraps them in a high-level SDK tailored for Vue 3.
7
+ > Note: this would made for personal and internal projects. If you find it useful, let me know.
8
8
 
9
- ### **Core Pillars**
9
+ ## **Features**
10
10
 
11
- 1. **Reactivity:** Deep integration with Vue's Composition API.
12
- 2. **Smart Invalidation:** Heuristic-based cache clearing. When a mutation (e.g., CreateTicket) succeeds, the SDK automatically invalidates related queries (e.g., ListTickets).
13
- 3. **Deep Pagination Search:** Recursively scans Protobuf messages for pagination fields to automatically switch from standard useQuery to useInfiniteQuery.
14
- 4. **Transport Decoupling:** Global interceptors for Auth and Error handling so your components stay clean.
11
+ - **Reactive Client:** Automatically re-initializes the transport when your base URL or auth tokens change.
12
+ - **TanStack Query Integration:** Generates useQuery, useMutation, and useInfiniteQuery hooks for every RPC.
13
+ - **Manual Wrappers:** Provides standard async wrappers for actions that don't fit the "query" pattern.
14
+ - **Message Initializers:** A createEmpty utility to generate default objects for any Protobuf message type.
15
+ - **Query Key Factory:** Centralized query keys to simplify manual cache invalidation.
15
16
 
16
- ---
17
-
18
- ## **🚀 Installation**
19
-
20
- ### **1. Plugin Installation**
21
-
22
- You can install the plugin globally or as a dev dependency.
17
+ ## **Installation**
23
18
 
24
- ```
25
- # Recommended for local development
26
- npm install --save-dev @zachacious/protoc-gen-connect-vue
19
+ ```bash
20
+ # install plugin either globally or locally
27
21
 
28
- # For use across non-Node projects (Go/Rust/etc)
29
22
  npm install -g @zachacious/protoc-gen-connect-vue
30
23
  ```
31
24
 
32
- ### **2. Peer Dependencies**
33
-
34
- The generated SDK requires these specific packages to be installed in your Vue project:
25
+ ```Bash
26
+ # install dependencies
35
27
 
36
- ```
37
- npm install @connectrpc/connect @connectrpc/connect-web @connectrpc/connect-query @tanstack/vue-query @bufbuild/protobuf
28
+ npm install protoc-gen-connect-vue @tanstack/vue-query @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf
38
29
  ```
39
30
 
40
- ---
31
+ ## **Generation**
41
32
 
42
- ## **⚙️ Configuration**
33
+ Add the plugin to your buf.gen.yaml:
43
34
 
44
- ### **buf.gen.yaml**
45
-
46
- The plugin **must** run after protoc-gen-es and protoc-gen-connect-query.
47
-
48
- ```yaml
49
- version: v2
50
- managed:
51
- enabled: true
35
+ ```YAML
52
36
  plugins:
53
- # 1. Base Protobuf TS messages
54
- - local: es
55
- out: gen
56
- opt: target=ts
57
- # 2. ConnectRPC Query definitions
58
- - local: connect-query
59
- out: gen
60
- opt: target=ts
61
- # 3. Smart SDK Generator
62
- - local: protoc-gen-connect-vue
63
- out: src/api
37
+ # ...
38
+ - remote: buf.build/bufbuild/es
39
+ out: web/src/api/gen
40
+ opt:
41
+ - target=ts
42
+
43
+ # Make sure this plugin goes after protoc-gen-es
44
+ # It should probably go last
45
+
46
+ # npm install -g @zachacious/protoc-gen-connect-vue
47
+ # https://www.npmjs.com/package/@zachacious/protoc-gen-connect-vue
48
+ - local: protoc-gen-connect-vue
49
+ out: web/src/api
50
+ opt: target=ts
64
51
  ```
65
52
 
66
- ---
67
-
68
- **🛠 Integration & Setup**
53
+ ## **Setup**
69
54
 
70
- ### **1. Global Client Configuration (main.ts)**
55
+ ### **1. Configure the Provider**
71
56
 
72
- Configure your environment-specific settings before mounting the app.
57
+ In your main.ts, provide the QueryClient to your Vue application.
73
58
 
74
59
  ```TypeScript
75
60
 
76
61
  import { createApp } from 'vue';
77
- import { VueQueryPlugin } from '@tanstack/vue-query';
78
- import { setBaseUrl, setAuthResolver, setSDKErrorCallback, globalQueryConfig } from '@/api';
79
- import { useAuthStore } from '@/stores/auth';
62
+ import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
63
+ import { globalQueryConfig } from './api/generated/client';
64
+ import App from './App.vue';
80
65
 
81
66
  const app = createApp(App);
67
+ const queryClient = new QueryClient(globalQueryConfig);
82
68
 
83
- // 1. Initialize TanStack with SDK defaults (StaleTime, GC, etc.)
84
- app.use(VueQueryPlugin, { queryClientConfig: globalQueryConfig });
85
-
86
- // 2. Configure Endpoint
87
- setBaseUrl(import.meta.env.VITE_API_URL);
69
+ app.use(VueQueryPlugin, { queryClient });
70
+ app.mount('#app');
71
+ ```
88
72
 
89
- // 3. Setup Auth Bridge (Decoupled from SDK logic)
90
- const auth = useAuthStore();
91
- setAuthResolver(async () => {
92
- return auth.token; // Header 'x-api-key' added if exists
93
- });
73
+ ### **2. Runtime Configuration**
94
74
 
95
- // 4. Global Error Handling
96
- setSDKErrorCallback((err, url) => {
97
- if (err.code === 16) { // Unauthenticated
98
- auth.logout();
99
- window.location.href = '/login';
100
- }
101
- });
75
+ Set up your authentication and base URL, typically in an identity store or main entry point.
102
76
 
103
- app.mount('#app');
104
- ```
77
+ ```TypeScript
105
78
 
106
- ### **2. Transport Provider Setup (App.vue)**
79
+ import { setBaseUrl, setAuthResolver, setSDKErrorCallback } from '@/api/generated';
107
80
 
108
- You must wrap your application (or relevant route views) in the TransportProvider to provide the ConnectRPC context to the hooks.
81
+ setBaseUrl('https://api.example.com');
109
82
 
110
- ```html
111
- <script setup lang="ts">
112
- import { TransportProvider } from "@connectrpc/connect-query";
113
- import { transport } from "@/api/client";
114
- </script>
83
+ // Supports async token resolution
84
+ setAuthResolver(async () => {
85
+ const token = localStorage.getItem('token');
86
+ return token ? token : null;
87
+ });
115
88
 
116
- <template>
117
- <TransportProvider :transport="transport">
118
- <router-view />
119
- </TransportProvider>
120
- </template>
89
+ setSDKErrorCallback((err, url) => {
90
+ console.error(`API Error at ${url}: ${err.message}`);
91
+ });
121
92
  ```
122
93
 
123
- ---
124
-
125
- ## **📖 Usage Examples**
94
+ ## **Usage**
126
95
 
127
- ### **Reactive Queries**
96
+ ### **Using Hooks and Manual Wrappers**
128
97
 
129
- Unary RPCs that don't start with mutation verbs are generated as standard queries.
98
+ The generated SDK provides both automated hooks and manual async functions.
130
99
 
131
100
  ```html
132
101
  <script setup lang="ts">
133
- import { useApi } from "@/api";
134
- const api = useApi();
135
-
136
- const { data, isLoading } = api.useGetCustomerById({ id: "123" });
102
+ import { ref } from "vue";
103
+ import { useApi } from "@/api/generated";
104
+
105
+ const { getUser, useGetUser } = useApi();
106
+
107
+ // 1. Reactive Hook (Auto-fetches)
108
+ const userId = ref("123");
109
+ const { data, isLoading, error } = useGetUser(userId);
110
+
111
+ // 2. Manual Action
112
+ const handleUpdate = async () => {
113
+ const { data, error } = await updateUser({ id: "123", name: "New Name" });
114
+ if (!error) {
115
+ // Note: The SDK automatically invalidates related queries on success
116
+ console.log("Update successful");
117
+ }
118
+ };
137
119
  </script>
138
120
 
139
121
  <template>
140
122
  <div v-if="isLoading">Loading...</div>
141
- <div v-else>{{ data.name }}</div>
123
+ <div v-else-if="error">{{ error }}</div>
124
+ <div v-else>
125
+ <h1>{{ data?.name }}</h1>
126
+ <button @click="handleUpdate">Update Profile</button>
127
+ </div>
142
128
  </template>
143
129
  ```
144
130
 
145
- ### **Automated Mutations & Invalidation**
146
-
147
- The SDK uses resource-name stripping (e.g., UpdateTicket -> Ticket) to invalidate active lists automatically.
131
+ ### **Initializing New Data (createEmpty)**
148
132
 
149
- ```html
150
- <script setup lang="ts">
151
- const { mutate, isPending } = api.useUpdateTicket({
152
- onSuccess: () => console.log("List refreshed by SDK!"),
153
- });
133
+ Protobuf messages often require specific default values (e.g., empty strings instead of undefined). The createEmpty utility ensures your local state matches the expected Protobuf structure.
154
134
 
155
- const save = () => mutate({ id: "123", status: "CLOSED" });
156
- </script>
157
- ```
158
-
159
- ### **Infinite Scrolling (Pagination)**
135
+ ```TypeScript
160
136
 
161
- If fields like page, offset, or limit are detected, the SDK generates an InfiniteQuery.
137
+ import { useApi } from '@/api/generated';
162
138
 
163
- ```html
164
- <script setup lang="ts">
165
- const { data, fetchNextPage, hasNextPage } = api.useListTickets({
166
- filter: "open",
167
- });
168
- </script>
139
+ const { createEmpty } = useApi();
169
140
 
170
- <template>
171
- <div v-for="page in data?.pages">
172
- <div v-for="ticket in page.items">{{ ticket.subject }}</div>
173
- </div>
174
- <button v-if="hasNextPage" @click="fetchNextPage">Load More</button>
175
- </template>
141
+ // Create an empty Customer object with all Protobuf defaults
142
+ // Useful when you need to create and empty instance instead of using a nullable object
143
+ const newCustomer = ref(createEmpty.Customer({ name: "Initial Name" }));
176
144
  ```
177
145
 
178
- ---
146
+ ### **Manual Cache Invalidation (queryKeys)**
179
147
 
180
- ## **🧪 Advanced Features**
148
+ If you need to manually refresh or invalidate a specific query, use the queryKeys factory to ensure the key matches the one used by the generated hooks.
181
149
 
182
- ### **Global Loading State**
150
+ ```TypeScript
183
151
 
184
- Track the status of _every_ RPC call in your application via a single computed property.
152
+ import { useQueryClient } from '@tanstack/vue-query';
153
+ import { queryKeys } from '@/api/generated';
185
154
 
186
- ```html
187
- <script setup lang="ts">
188
- const { isGlobalLoading } = useApi();
189
- </script>
155
+ const queryClient = useQueryClient();
190
156
 
191
- <template>
192
- <ProgressBar v-if="isGlobalLoading" />
193
- </template>
157
+ const refreshUser = (id: string) => {
158
+ queryClient.invalidateQueries({
159
+ queryKey: queryKeys.getUser(id)
160
+ });
161
+ };
194
162
  ```
195
163
 
196
- ### **Protobuf Documentation Tags**
164
+ ## **Global Loading State**
197
165
 
198
- Fine-tune your SDK directly from your .proto comments:
166
+ The SDK exports a reactive isGlobalLoading computed property that tracks if any RPC (query or mutation) is currently in flight.
199
167
 
200
- | Tag | Description |
201
- | :------------------- | :--------------------------------------------------------------- |
202
- | @wrapper:auth | Marks the endpoint as expecting authentication in documentation. |
203
- | @sdk:signature=(...) | Overrides the generated TypeScript function signature. |
204
- | @sdk:data=res.item | Overrides the default data extractor for async wrappers. |
168
+ ```TypeScript
205
169
 
206
- ---
170
+ const { isGlobalLoading } = useApi();
171
+ ```
207
172
 
208
- **📝 License**
173
+ ---
209
174
 
210
- MIT © [Zachacious](https://www.google.com/search?q=https://github.com/zachacious)
175
+ License MIT
package/dist/generator.js CHANGED
@@ -336845,14 +336845,28 @@ function processType(typeDesc, serviceFile, wktImports, localImports, externalIm
336845
336845
  externalImports.get(importPath).add(baseName);
336846
336846
  return baseName;
336847
336847
  }
336848
+ function collectAllMessages(message, serviceFile, wktImports, localImports, externalImports, seen = new Set) {
336849
+ if (seen.has(message.typeName))
336850
+ return;
336851
+ seen.add(message.typeName);
336852
+ processType(message, serviceFile, wktImports, localImports, externalImports);
336853
+ for (const field of message.fields) {
336854
+ if (field.fieldKind === "message") {
336855
+ collectAllMessages(field.message, serviceFile, wktImports, localImports, externalImports, seen);
336856
+ }
336857
+ }
336858
+ }
336848
336859
  function processService(service, protoPbFile, connectQueryFile) {
336849
336860
  const rpcs = [];
336850
336861
  const wktImports = new Set;
336851
336862
  const localImports = new Set;
336852
336863
  const externalImports = new Map;
336864
+ const allSeenMessages = new Set;
336853
336865
  for (const method of service.methods) {
336854
- const inputBaseName = processType(method.input, service.file, wktImports, localImports, externalImports);
336855
- const outputBaseName = processType(method.output, service.file, wktImports, localImports, externalImports);
336866
+ collectAllMessages(method.input, service.file, wktImports, localImports, externalImports, allSeenMessages);
336867
+ collectAllMessages(method.output, service.file, wktImports, localImports, externalImports, allSeenMessages);
336868
+ const inputBaseName = method.input.name;
336869
+ const outputBaseName = method.output.name;
336856
336870
  const camelName = method.name.charAt(0).toLowerCase() + method.name.slice(1);
336857
336871
  const mutationVerbs = [
336858
336872
  "Create",
@@ -336884,11 +336898,15 @@ function processService(service, protoPbFile, connectQueryFile) {
336884
336898
  isPaginated: isPaginatedDeep(method.input) && isUnary && !isMutation
336885
336899
  });
336886
336900
  }
336901
+ const messageNames = Array.from(allSeenMessages).map((fullPath) => {
336902
+ return fullPath.split(".").pop();
336903
+ });
336887
336904
  return {
336888
336905
  serviceName: service.name,
336889
336906
  protoPbFile,
336890
336907
  connectQueryFile,
336891
336908
  rpcs,
336909
+ messageNames,
336892
336910
  wktImports: Array.from(wktImports),
336893
336911
  localImports: Array.from(localImports),
336894
336912
  externalImports: Array.from(externalImports.entries()).map(([path2, types2]) => ({
@@ -336899,7 +336917,7 @@ function processService(service, protoPbFile, connectQueryFile) {
336899
336917
  }
336900
336918
  var plugin = createEcmaScriptPlugin({
336901
336919
  name: "protoc-gen-connect-vue",
336902
- version: "v1.0.2",
336920
+ version: "v1.0.3",
336903
336921
  generateTs: (schema) => {
336904
336922
  let firstService = schema.files.flatMap((f) => f.services)[0];
336905
336923
  if (!firstService)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachacious/protoc-gen-connect-vue",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Smart TanStack Query & ConnectRPC SDK generator for Vue.js",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,17 +1,28 @@
1
- import { computed } from "vue";
2
- import { useQuery, useMutation, useInfiniteQuery, useQueryClient, useIsFetching, useIsMutating } from "@tanstack/vue-query";
1
+ import { computed, unref } from "vue";
2
+ import {
3
+ useQuery,
4
+ useMutation,
5
+ useInfiniteQuery,
6
+ useQueryClient,
7
+ useIsFetching,
8
+ useIsMutating
9
+ } from "@tanstack/vue-query";
3
10
  import { ConnectError } from "@connectrpc/connect";
11
+ import { create } from "@bufbuild/protobuf";
4
12
  import { useGrpcClient } from "./client";
5
13
 
6
- import {
7
- {{#rpcs}}
8
- {{queryDefinitionName}} as {{queryDefinitionName}}Def,
9
- {{/rpcs}}
10
- } from "./gen/{{{connectQueryFile}}}";
14
+ {{#wktImports}}
15
+ import { {{.}}Schema } from "@bufbuild/protobuf";
16
+ import type { {{.}} } from "@bufbuild/protobuf";
17
+ {{/wktImports}}
18
+
19
+ {{#localImports}}
20
+ import { {{.}}Schema } from "./gen/{{{protoPbFile}}}";
21
+ import type { {{.}} } from "./gen/{{{protoPbFile}}}";
22
+ {{/localImports}}
11
23
 
12
- {{#wktImports}}import type { {{.}} } from "@bufbuild/protobuf";{{/wktImports}}
13
- {{#localImports}}import type { {{.}} } from "./gen/{{{protoPbFile}}}";{{/localImports}}
14
24
  {{#externalImports}}
25
+ import { {{#types}}{{.}}Schema, {{/types}} } from "{{{path}}}";
15
26
  import type { {{#types}}{{.}}, {{/types}} } from "{{{path}}}";
16
27
  {{/externalImports}}
17
28
 
@@ -33,6 +44,25 @@ async function callConnect<ReqT, ResT, DataT>(
33
44
  }
34
45
  }
35
46
 
47
+ /**
48
+ * Query Keys for manual cache management
49
+ */
50
+ export const queryKeys = {
51
+ {{#rpcs}}
52
+ {{functionName}}: (input?: any) => ["{{resource}}", "{{functionName}}", input] as const,
53
+ {{/rpcs}}
54
+ };
55
+
56
+ /**
57
+ * Utility to create default/empty versions of Protobuf messages
58
+ * ensuring all fields (even nested ones) are initialized per schema.
59
+ */
60
+ export const createEmpty = {
61
+ {{#messageNames}}
62
+ {{.}}: (data?: Partial<{{.}}>) => create({{.}}Schema, data),
63
+ {{/messageNames}}
64
+ };
65
+
36
66
  export const useApi = () => {
37
67
  const { client } = useGrpcClient();
38
68
  const queryClient = useQueryClient();
@@ -52,5 +82,7 @@ export const useApi = () => {
52
82
  {{hookName}},
53
83
  {{/rpcs}}
54
84
  isGlobalLoading,
85
+ queryKeys,
86
+ createEmpty
55
87
  };
56
88
  };
@@ -1,14 +1,25 @@
1
+ import { ref, computed } from "vue";
1
2
  import { {{serviceName}} } from './gen/{{{protoPbFile}}}';
2
3
  import { createClient, ConnectError, type Interceptor } from "@connectrpc/connect";
3
4
  import { createConnectTransport } from "@connectrpc/connect-web";
4
5
 
5
- let baseUrl = 'http://localhost:3000';
6
+ // This workaround insures that bigints returned from the backend
7
+ // can be serialized - otherwise you will run into some hard to pin down bugs
8
+ // dealing with bigint serialization
9
+ if (!(BigInt.prototype as any).toJSON) {
10
+ (BigInt.prototype as any).toJSON = function () {
11
+ return this.toString();
12
+ };
13
+ }
14
+
15
+ // --- REACTIVE STATE ---
16
+ const baseUrl = ref('http://localhost:3000');
6
17
 
7
18
  /**
8
19
  * Configure the API endpoint at runtime.
9
20
  */
10
21
  export const setBaseUrl = (url: string) => {
11
- baseUrl = url;
22
+ baseUrl.value = url;
12
23
  };
13
24
 
14
25
  // --- AUTH RESOLVER ---
@@ -35,12 +46,20 @@ const transportInterceptor: Interceptor = (next) => async (req) => {
35
46
  }
36
47
  };
37
48
 
38
- export const transport = createConnectTransport({
39
- baseUrl,
40
- interceptors: [transportInterceptor],
41
- });
49
+ /**
50
+ * useGrpcClient provides a reactive client.
51
+ * If baseUrl.value changes, the computed client updates automatically.
52
+ */
53
+ export const useGrpcClient = () => {
54
+ const transport = computed(() => createConnectTransport({
55
+ baseUrl: baseUrl.value,
56
+ interceptors: [transportInterceptor],
57
+ }));
58
+
59
+ const client = computed(() => createClient({{serviceName}}, transport.value));
42
60
 
43
- export const client = createClient({{serviceName}}, transport);
61
+ return { client };
62
+ };
44
63
 
45
64
  export const globalQueryConfig = {
46
65
  defaultOptions: {
@@ -52,8 +71,6 @@ export const globalQueryConfig = {
52
71
  },
53
72
  };
54
73
 
55
- export const useGrpcClient = () => ({ client });
56
-
57
74
  /*
58
75
 
59
76
  // the following is an example of how to use the hybrid api
@@ -1,50 +1,59 @@
1
1
  /**
2
- * Standard Async: {{functionName}}
3
- */
4
- const {{functionName}} = async (req: {{inputType}}): Promise<APIResponse<{{outputType}}>> => {
5
- const res = await callConnect(client.{{functionName}}, req, (res) => res);
6
- if (!res.error) queryClient.invalidateQueries({ queryKey: ["{{resource}}"] });
7
- return res;
8
- };
2
+ * Standard Async: {{functionName}}
3
+ * Used for manual actions. Invalidates the cache for this resource on success.
4
+ */
5
+ const {{functionName}} = async (req: {{inputType}}): Promise<APIResponse<{{outputType}}>> => {
6
+ const res = await callConnect(client.value.{{functionName}}.bind(client.value), req, (res) => res);
7
+ if (!res.error) {
8
+ queryClient.invalidateQueries({ queryKey: ["{{resource}}"] });
9
+ }
10
+ return res;
11
+ };
9
12
 
10
- /**
11
- * TanStack Hook: {{hookName}}
12
- */
13
- const {{hookName}} = (
14
- {{#isQuery}}
15
- input: {{inputType}},
16
- options: any = {}
17
- {{/isQuery}}
18
- {{^isQuery}}
19
- options: any = {}
20
- {{/isQuery}}
21
- ) => {
22
- {{#isPaginated}}
23
- return useInfiniteQuery({
24
- ...{{queryDefinitionName}}Def,
25
- ...options,
26
- queryKey: ["{{resource}}", "{{functionName}}", input],
27
- initialPageParam: 1,
28
- getNextPageParam: options.getNextPageParam || ((lastPage: any) => lastPage.nextPage ?? undefined),
29
- });
30
- {{/isPaginated}}
31
- {{^isPaginated}}
32
- {{#isQuery}}
33
- return useQuery({
34
- ...{{queryDefinitionName}}Def,
35
- ...options,
36
- queryKey: ["{{resource}}", "{{functionName}}", input],
37
- });
38
- {{/isQuery}}
39
- {{^isQuery}}
40
- return useMutation({
41
- ...{{queryDefinitionName}}Def,
42
- ...options,
43
- onSuccess: async (data, variables, context) => {
44
- await queryClient.invalidateQueries({ queryKey: ["{{resource}}"] });
45
- if (options.onSuccess) return options.onSuccess(data, variables, context);
46
- },
47
- });
48
- {{/isQuery}}
49
- {{/isPaginated}}
50
- };
13
+ /**
14
+ * TanStack Hook: {{hookName}}
15
+ * Provides reactive data binding with caching.
16
+ */
17
+ const {{hookName}} = (
18
+ {{#isQuery}}
19
+ input: any, // Accepts Ref<{{inputType}}> or {{inputType}}
20
+ options: any = {}
21
+ {{/isQuery}}
22
+ {{^isQuery}}
23
+ options: any = {}
24
+ {{/isQuery}}
25
+ ) => {
26
+ {{#isPaginated}}
27
+ return useInfiniteQuery({
28
+ queryKey: queryKeys.{{functionName}}(input),
29
+ queryFn: async ({ pageParam }) => {
30
+ const req = { ...unref(input), page: pageParam };
31
+ return client.value.{{functionName}}(req);
32
+ },
33
+ initialPageParam: 1,
34
+ getNextPageParam: (lastPage: any) => lastPage.nextPage ?? undefined,
35
+ ...options,
36
+ });
37
+ {{/isPaginated}}
38
+
39
+ {{^isPaginated}}
40
+ {{#isQuery}}
41
+ return useQuery({
42
+ queryKey: queryKeys.{{functionName}}(input),
43
+ queryFn: () => client.value.{{functionName}}(unref(input)),
44
+ ...options,
45
+ });
46
+ {{/isQuery}}
47
+
48
+ {{^isQuery}}
49
+ return useMutation({
50
+ mutationFn: (req: {{inputType}}) => client.value.{{functionName}}(req),
51
+ onSuccess: async (data, variables, context) => {
52
+ await queryClient.invalidateQueries({ queryKey: ["{{resource}}"] });
53
+ if (options.onSuccess) return options.onSuccess(data, variables, context);
54
+ },
55
+ ...options,
56
+ });
57
+ {{/isQuery}}
58
+ {{/isPaginated}}
59
+ };