@cushin/api-codegen 1.0.3 → 1.0.5

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/dist/cli.js CHANGED
@@ -119,6 +119,49 @@ var BaseGenerator = class {
119
119
  return `return apiClient.${name}();`;
120
120
  }
121
121
  }
122
+ inferNonNull(expr) {
123
+ return `z.infer<NonNullable<${expr}>>`;
124
+ }
125
+ toCamelCase(str) {
126
+ return str.toLowerCase().replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toLowerCase());
127
+ }
128
+ getResourceFromEndpoint(_name, endpoint) {
129
+ const tag = endpoint.tags?.find((t) => t !== "query" && t !== "mutation");
130
+ if (tag) return this.toCamelCase(tag);
131
+ const match = endpoint.path.match(/^\/([^/]+)/);
132
+ return match ? this.toCamelCase(match[1]) : "general";
133
+ }
134
+ groupEndpointsByResource() {
135
+ const groups = {};
136
+ Object.entries(this.context.apiConfig.endpoints).forEach(
137
+ ([name, endpoint]) => {
138
+ const res = this.getResourceFromEndpoint(name, endpoint);
139
+ if (!groups[res]) groups[res] = [];
140
+ groups[res].push({ name, endpoint });
141
+ }
142
+ );
143
+ return groups;
144
+ }
145
+ resourceHasQueryEndpoints(resource) {
146
+ return this.groupEndpointsByResource()[resource]?.some(
147
+ ({ endpoint }) => endpoint.method === "GET"
148
+ ) ?? false;
149
+ }
150
+ getEndpointKeyName(name) {
151
+ return name.startsWith("get") ? name[3].toLowerCase() + name.slice(4) : name;
152
+ }
153
+ generateQueryKeyCall(resource, name, endpoint) {
154
+ const key = this.getEndpointKeyName(name);
155
+ const args = [];
156
+ if (endpoint.params) args.push("params");
157
+ if (endpoint.query) args.push("filters");
158
+ return args.length ? `queryKeys.${resource}.${key}(${args.join(", ")})` : `queryKeys.${resource}.${key}()`;
159
+ }
160
+ hasQueryOptions() {
161
+ return Object.values(this.context.apiConfig.endpoints).some(
162
+ (e) => e.method === "GET"
163
+ );
164
+ }
122
165
  };
123
166
 
124
167
  // src/generators/hooks.ts
@@ -131,100 +174,132 @@ var HooksGenerator = class extends BaseGenerator {
131
174
  }
132
175
  generateContent() {
133
176
  const useClientDirective = this.context.config.options?.useClientDirective ?? true;
134
- const imports = `${useClientDirective ? "'use client';\n" : ""}
135
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
136
- import type {
137
- UseQueryOptions,
138
- UseMutationOptions,
139
- QueryKey
140
- } from '@tanstack/react-query';
141
- import { apiClient } from './client';
142
- import type {
143
- APIEndpoints,
144
- ExtractBody,
145
- ExtractParams,
146
- ExtractQuery,
147
- ExtractResponse
148
- } from './types';
177
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
178
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
179
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
180
+ const content = `${useClientDirective ? "'use client';\n" : ""}
181
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
182
+ import { apiClient } from "./client";
183
+ import { queryKeys } from "./query-keys";
184
+ import { apiQueryOptions } from "./query-options";
185
+ import { z } from "zod";
186
+ import { apiConfig } from "${relativePath}";
187
+
188
+ ${this.generateQueryHooks()}
189
+ ${this.generateMutationHooks()}
149
190
  `;
191
+ return content;
192
+ }
193
+ generateQueryHooks() {
150
194
  const hooks = [];
151
- Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
152
- if (this.isQueryEndpoint(endpoint)) {
153
- hooks.push(this.generateQueryHook(name, endpoint));
154
- } else {
155
- hooks.push(this.generateMutationHook(name, endpoint));
195
+ Object.entries(this.context.apiConfig.endpoints).forEach(
196
+ ([name, endpoint]) => {
197
+ if (endpoint.method === "GET")
198
+ hooks.push(this.generateQueryHook(name, endpoint));
156
199
  }
157
- });
158
- return imports + "\n" + hooks.join("\n\n");
200
+ );
201
+ return hooks.join("\n\n");
159
202
  }
160
203
  generateQueryHook(name, endpoint) {
161
- const hookPrefix = this.context.config.options?.hookPrefix || "use";
162
- const hookName = `${hookPrefix}${this.capitalize(name)}`;
163
- const signature = this.getEndpointSignature(name, endpoint);
204
+ const hookName = `use${this.capitalize(name)}`;
205
+ const resource = this.getResourceFromEndpoint(name, endpoint);
206
+ const optionName = this.getEndpointKeyName(name);
207
+ const inferParams = this.inferNonNull(
208
+ `typeof apiConfig.endpoints.${name}.params`
209
+ );
210
+ const inferQuery = this.inferNonNull(
211
+ `typeof apiConfig.endpoints.${name}.query`
212
+ );
213
+ const inferResponse = this.inferNonNull(
214
+ `typeof apiConfig.endpoints.${name}.response`
215
+ );
216
+ const params = [];
217
+ const optionParams = [];
164
218
  const queryTags = this.getQueryTags(endpoint);
165
- const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
166
- const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
167
- const optionsDef = `options?: Omit<UseQueryOptions<${signature.responseType}, Error, ${signature.responseType}, QueryKey>, 'queryKey' | 'queryFn'>`;
168
- const paramsList = [paramDef, queryDef, optionsDef].filter(Boolean).join(",\n ");
169
- const queryKeyParts = [
170
- ...queryTags.map((tag) => `'${tag}'`),
171
- signature.hasParams ? "params" : "undefined",
172
- signature.hasQuery ? "query" : "undefined"
173
- ];
174
- const clientCallArgs = [];
175
- if (signature.hasParams) clientCallArgs.push("params");
176
- if (signature.hasQuery) clientCallArgs.push("query");
219
+ if (endpoint.params) {
220
+ params.push(`params: ${inferParams}`);
221
+ optionParams.push("params");
222
+ }
223
+ if (endpoint.query) {
224
+ params.push(`filters?: ${inferQuery}`);
225
+ optionParams.push("filters");
226
+ }
227
+ params.push(`options?: {
228
+ enabled?: boolean;
229
+ select?: <TData = ${inferResponse}>(data: ${inferResponse}) => TData;
230
+ }`);
177
231
  return `/**
178
232
  * ${endpoint.description || `Query hook for ${name}`}
179
233
  * @tags ${queryTags.join(", ") || "none"}
180
234
  */
181
- export function ${hookName}(
182
- ${paramsList}
183
- ) {
235
+ export function ${hookName}(${params.join(",\n ")}) {
184
236
  return useQuery({
185
- queryKey: [${queryKeyParts.join(", ")}] as const,
186
- queryFn: () => apiClient.${name}(${clientCallArgs.join(", ")}),
237
+ ...apiQueryOptions.${resource}.${optionName}(${optionParams.join(", ")}),
187
238
  ...options,
188
239
  });
189
240
  }`;
190
241
  }
242
+ generateMutationHooks() {
243
+ const hooks = [];
244
+ Object.entries(this.context.apiConfig.endpoints).forEach(
245
+ ([name, endpoint]) => {
246
+ if (endpoint.method !== "GET")
247
+ hooks.push(this.generateMutationHook(name, endpoint));
248
+ }
249
+ );
250
+ return hooks.join("\n\n");
251
+ }
191
252
  generateMutationHook(name, endpoint) {
192
- const hookPrefix = this.context.config.options?.hookPrefix || "use";
193
- const hookName = `${hookPrefix}${this.capitalize(name)}`;
194
- const signature = this.getEndpointSignature(name, endpoint);
195
- const invalidationTags = this.getInvalidationTags(endpoint);
196
- let inputType = "void";
197
- if (signature.hasParams && signature.hasBody) {
198
- inputType = `{ params: ${signature.paramType}; body: ${signature.bodyType} }`;
199
- } else if (signature.hasParams) {
200
- inputType = signature.paramType;
201
- } else if (signature.hasBody) {
202
- inputType = signature.bodyType;
253
+ const hookName = `use${this.capitalize(name)}`;
254
+ const resource = this.getResourceFromEndpoint(name, endpoint);
255
+ const inferParams = this.inferNonNull(
256
+ `typeof apiConfig.endpoints.${name}.params`
257
+ );
258
+ const inferBody = this.inferNonNull(
259
+ `typeof apiConfig.endpoints.${name}.body`
260
+ );
261
+ const inferResponse = this.inferNonNull(
262
+ `typeof apiConfig.endpoints.${name}.response`
263
+ );
264
+ const resourceHasQueries = this.resourceHasQueryEndpoints(resource);
265
+ let inputType;
266
+ let fnBody;
267
+ if (endpoint.params && endpoint.body) {
268
+ inputType = `{ params: ${inferParams}; body: ${inferBody}; }`;
269
+ fnBody = `({ params, body }: ${inputType}) => apiClient.${name}(params, body)`;
270
+ } else if (endpoint.params) {
271
+ inputType = `${inferParams}`;
272
+ fnBody = `(params: ${inputType}) => apiClient.${name}(params)`;
273
+ } else if (endpoint.body) {
274
+ inputType = `${inferBody}`;
275
+ fnBody = `(body: ${inputType}) => apiClient.${name}(body)`;
276
+ } else {
277
+ inputType = "void";
278
+ fnBody = `() => apiClient.${name}()`;
203
279
  }
204
- const invalidationQueries = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` queryClient.invalidateQueries({ queryKey: ['${tag}'] });`).join("\n") : " // No automatic invalidations";
280
+ const invalidate = resourceHasQueries ? `queryClient.invalidateQueries({ queryKey: queryKeys.${resource}.all });` : "";
205
281
  return `/**
206
282
  * ${endpoint.description || `Mutation hook for ${name}`}
207
283
  * @tags ${endpoint.tags?.join(", ") || "none"}
208
284
  */
209
- export function ${hookName}(
210
- options?: Omit<UseMutationOptions<${signature.responseType}, Error, ${inputType}>, 'mutationFn'>
211
- ) {
212
- const queryClient = useQueryClient();
213
-
214
- return useMutation({
215
- mutationFn: ${inputType === "void" ? "() => {" : "(input) => {"}
216
- ${this.generateMutationCall(name, signature.hasParams, signature.hasBody)}
217
- },
218
- onSuccess: (data, variables, context) => {
219
- // Invalidate related queries
220
- ${invalidationQueries}
221
-
222
- // Call user's onSuccess if provided
223
- options?.onSuccess?.(data, variables, context);
224
- },
225
- ...options,
226
- });
227
- }`;
285
+ export function ${hookName}(options?: {
286
+ onSuccess?: (data: ${inferResponse}, variables: ${inputType}, context: unknown) => void;
287
+ onError?: (error: Error, variables: ${inputType}, context: unknown) => void;
288
+ onSettled?: (data: ${inferResponse} | undefined, error: Error | null, variables: ${inputType}, context: unknown) => void;
289
+ onMutate?: (variables: ${inputType}) => Promise<unknown> | unknown;
290
+ }) {
291
+ ${invalidate ? "const queryClient = useQueryClient();" : ""}
292
+ return useMutation({
293
+ mutationFn: ${fnBody},
294
+ onSuccess: (data, variables, context) => {
295
+ ${invalidate}
296
+ options?.onSuccess?.(data, variables, context);
297
+ },
298
+ onError: options?.onError,
299
+ onSettled: options?.onSettled,
300
+ onMutate: options?.onMutate,
301
+ });
302
+ }`;
228
303
  }
229
304
  };
230
305
  var ServerActionsGenerator = class extends BaseGenerator {
@@ -319,11 +394,13 @@ import type {
319
394
  } from './types';
320
395
  `;
321
396
  const queries = [];
322
- Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
323
- if (this.isQueryEndpoint(endpoint)) {
324
- queries.push(this.generateServerQuery(name, endpoint));
397
+ Object.entries(this.context.apiConfig.endpoints).forEach(
398
+ ([name, endpoint]) => {
399
+ if (this.isQueryEndpoint(endpoint)) {
400
+ queries.push(this.generateServerQuery(name, endpoint));
401
+ }
325
402
  }
326
- });
403
+ );
327
404
  return imports + "\n" + queries.join("\n\n");
328
405
  }
329
406
  generateServerQuery(name, endpoint) {
@@ -336,9 +413,7 @@ import type {
336
413
  const clientCallArgs = [];
337
414
  if (signature.hasParams) clientCallArgs.push("params");
338
415
  if (signature.hasQuery) clientCallArgs.push("query");
339
- const cacheKeyParts = [
340
- `'${name}'`
341
- ];
416
+ const cacheKeyParts = [`'${name}'`];
342
417
  if (signature.hasParams) cacheKeyParts.push("JSON.stringify(params)");
343
418
  if (signature.hasQuery) cacheKeyParts.push("JSON.stringify(query)");
344
419
  return `/**
@@ -367,13 +442,18 @@ var TypesGenerator = class extends BaseGenerator {
367
442
  await fs5.writeFile(outputPath, content, "utf-8");
368
443
  }
369
444
  generateContent() {
445
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
446
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
447
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
370
448
  return `// Auto-generated type definitions
371
449
  // Do not edit this file manually
372
450
 
373
451
  import type { z } from 'zod';
452
+ import { apiConfig } from '${relativePath}';
453
+
374
454
 
375
455
  // Re-export endpoint configuration types
376
- export type { APIConfig, APIEndpoint, HTTPMethod } from '@vietbus/api-codegen/config';
456
+ export type { APIConfig, APIEndpoint, HTTPMethod } from '@cushin/api-codegen/schema';
377
457
 
378
458
  /**
379
459
  * Type helper to extract params schema from an endpoint
@@ -406,13 +486,37 @@ export type ExtractResponse<T> = T extends { response: infer R extends z.ZodType
406
486
  /**
407
487
  * Import your API config to get typed endpoints
408
488
  *
409
- * @example
410
- * import { apiConfig } from './config/endpoints';
411
- * export type APIEndpoints = typeof apiConfig.endpoints;
412
489
  */
413
- export type APIEndpoints = Record<string, any>;
490
+ export type APIEndpoints = typeof apiConfig.endpoints;
491
+
492
+ ${this.generateEndpointTypes()}
414
493
  `;
415
494
  }
495
+ generateEndpointTypes() {
496
+ const types = [];
497
+ Object.entries(this.context.apiConfig.endpoints).forEach(
498
+ ([name, endpoint]) => {
499
+ const cap = this.capitalize(name);
500
+ if (endpoint.response)
501
+ types.push(
502
+ `export type ${cap}Response = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.response`)};`
503
+ );
504
+ if (endpoint.body)
505
+ types.push(
506
+ `export type ${cap}Input = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.body`)};`
507
+ );
508
+ if (endpoint.query)
509
+ types.push(
510
+ `export type ${cap}Query = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.query`)};`
511
+ );
512
+ if (endpoint.params)
513
+ types.push(
514
+ `export type ${cap}Params = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.params`)};`
515
+ );
516
+ }
517
+ );
518
+ return types.join("\n");
519
+ }
416
520
  };
417
521
  var ClientGenerator = class extends BaseGenerator {
418
522
  async generate() {
@@ -429,51 +533,62 @@ var ClientGenerator = class extends BaseGenerator {
429
533
  }
430
534
  async generateServerClientFile() {
431
535
  const content = this.generateServerClientContent();
432
- const outputPath = path6.join(this.context.config.outputDir, "server-client.ts");
536
+ const outputPath = path6.join(
537
+ this.context.config.outputDir,
538
+ "server-client.ts"
539
+ );
433
540
  await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
434
541
  await fs5.writeFile(outputPath, content, "utf-8");
435
542
  }
436
543
  generateClientContent() {
437
544
  const useClientDirective = this.context.config.options?.useClientDirective ?? true;
545
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
546
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
547
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
438
548
  return `${useClientDirective ? "'use client';\n" : ""}
439
549
  import { createAPIClient } from '@cushin/api-codegen/client';
440
550
  import type { AuthCallbacks } from '@cushin/api-codegen/client';
441
- import { apiConfig } from '../config/endpoints';
442
- import type { APIEndpoints } from './types';
551
+ import { apiConfig } from '${relativePath}';
552
+ import { z } from 'zod';
443
553
 
444
- // Type-safe API client methods
554
+ // Type the methods based on endpoints
445
555
  type APIClientMethods = {
446
- [K in keyof APIEndpoints]: APIEndpoints[K] extends {
556
+ [K in keyof typeof apiConfig.endpoints]: (typeof apiConfig.endpoints)[K] extends {
447
557
  method: infer M;
448
558
  params?: infer P;
449
559
  query?: infer Q;
450
560
  body?: infer B;
451
561
  response: infer R;
452
562
  }
453
- ? M extends 'GET'
454
- ? P extends { _type: any }
455
- ? Q extends { _type: any }
456
- ? (params: P['_type'], query?: Q['_type']) => Promise<R['_type']>
457
- : (params: P['_type']) => Promise<R['_type']>
458
- : Q extends { _type: any }
459
- ? (query?: Q['_type']) => Promise<R['_type']>
460
- : () => Promise<R['_type']>
461
- : P extends { _type: any }
462
- ? B extends { _type: any }
463
- ? (params: P['_type'], body: B['_type']) => Promise<R['_type']>
464
- : (params: P['_type']) => Promise<R['_type']>
465
- : B extends { _type: any }
466
- ? (body: B['_type']) => Promise<R['_type']>
467
- : () => Promise<R['_type']>
563
+ ? M extends "GET"
564
+ ? P extends z.ZodJSONSchema
565
+ ? Q extends z.ZodJSONSchema
566
+ ? (params: z.infer<P>, query?: z.infer<Q>) => Promise<z.infer<R>>
567
+ : (params: z.infer<P>) => Promise<z.infer<R>>
568
+ : Q extends z.ZodJSONSchema
569
+ ? (query?: z.infer<Q>) => Promise<z.infer<R>>
570
+ : () => Promise<z.infer<R>>
571
+ : P extends z.ZodJSONSchema
572
+ ? B extends z.ZodJSONSchema
573
+ ? (params: z.infer<P>, body: z.infer<B>) => Promise<z.infer<R>>
574
+ : (params: z.infer<P>) => Promise<z.infer<R>>
575
+ : B extends z.ZodJSONSchema
576
+ ? (body: z.infer<B>) => Promise<z.infer<R>>
577
+ : () => Promise<z.infer<R>>
468
578
  : never;
469
579
  };
470
580
 
581
+
471
582
  // Export singleton instance (will be initialized later)
472
- export let apiClient: APIClientMethods & {
583
+ export let baseClient: APIClientMethods & {
473
584
  refreshAuth: () => Promise<void>;
474
585
  updateAuthCallbacks: (callbacks: AuthCallbacks) => void;
475
586
  };
476
587
 
588
+ export const apiClient = {
589
+ ${this.generateApiClientMethods()}
590
+ };
591
+
477
592
  /**
478
593
  * Initialize API client with auth callbacks
479
594
  * Call this function in your auth provider setup
@@ -493,8 +608,8 @@ export let apiClient: APIClientMethods & {
493
608
  * initializeAPIClient(authCallbacks);
494
609
  */
495
610
  export const initializeAPIClient = (authCallbacks: AuthCallbacks) => {
496
- apiClient = createAPIClient(apiConfig, authCallbacks) as any;
497
- return apiClient;
611
+ baseClient = createAPIClient(apiConfig, authCallbacks) as any;
612
+ return baseClient;
498
613
  };
499
614
 
500
615
  // Export for custom usage
@@ -541,6 +656,204 @@ type APIClientMethods = {
541
656
  export const serverClient = createAPIClient(apiConfig) as APIClientMethods;
542
657
  `;
543
658
  }
659
+ generateApiClientMethods() {
660
+ const methods = [];
661
+ Object.entries(this.context.apiConfig.endpoints).forEach(
662
+ ([name, endpoint]) => {
663
+ const inferParams = this.inferNonNull(
664
+ `typeof apiConfig.endpoints.${name}.params`
665
+ );
666
+ const inferQuery = this.inferNonNull(
667
+ `typeof apiConfig.endpoints.${name}.query`
668
+ );
669
+ const inferBody = this.inferNonNull(
670
+ `typeof apiConfig.endpoints.${name}.body`
671
+ );
672
+ const inferResponse = this.inferNonNull(
673
+ `typeof apiConfig.endpoints.${name}.response`
674
+ );
675
+ if (endpoint.method === "GET") {
676
+ if (endpoint.params && endpoint.query) {
677
+ methods.push(` ${name}: (params: ${inferParams}, query?: ${inferQuery}): Promise<${inferResponse}> =>
678
+ (baseClient as any).${name}(params, query),`);
679
+ } else if (endpoint.params) {
680
+ methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
681
+ (baseClient as any).${name}(params),`);
682
+ } else if (endpoint.query) {
683
+ methods.push(` ${name}: (query?: ${inferQuery}): Promise<${inferResponse}> =>
684
+ (baseClient as any).${name}(query),`);
685
+ } else {
686
+ methods.push(` ${name}: (): Promise<${inferResponse}> =>
687
+ (baseClient as any).${name}(),`);
688
+ }
689
+ } else {
690
+ if (endpoint.params && endpoint.body) {
691
+ methods.push(` ${name}: (params: ${inferParams}, body: ${inferBody}): Promise<${inferResponse}> =>
692
+ (baseClient as any).${name}(params, body),`);
693
+ } else if (endpoint.params) {
694
+ methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
695
+ (baseClient as any).${name}(params),`);
696
+ } else if (endpoint.body) {
697
+ methods.push(` ${name}: (body: ${inferBody}): Promise<${inferResponse}> =>
698
+ (baseClient as any).${name}(body),`);
699
+ } else {
700
+ methods.push(` ${name}: (): Promise<${inferResponse}> =>
701
+ (baseClient as any).${name}(),`);
702
+ }
703
+ }
704
+ }
705
+ );
706
+ return methods.join("\n");
707
+ }
708
+ };
709
+ var QueryKeysGenerator = class extends BaseGenerator {
710
+ async generate() {
711
+ const content = this.generateContent();
712
+ const outputPath = path6.join(
713
+ this.context.config.outputDir,
714
+ "query-keys.ts"
715
+ );
716
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
717
+ await fs5.writeFile(outputPath, content, "utf-8");
718
+ }
719
+ generateContent() {
720
+ const content = `// Auto-generated query keys
721
+ import { z } from 'zod';
722
+ import { apiConfig } from '../config/endpoints';
723
+
724
+ export const queryKeys = {
725
+ ${this.generateQueryKeysContent()}
726
+ } as const;
727
+ `;
728
+ return content;
729
+ }
730
+ generateQueryKeysContent() {
731
+ const resourceGroups = this.groupEndpointsByResource();
732
+ const keys = [];
733
+ Object.entries(resourceGroups).forEach(([resource, endpoints]) => {
734
+ const queryEndpoints = endpoints.filter(
735
+ ({ endpoint }) => endpoint.method === "GET"
736
+ );
737
+ if (queryEndpoints.length === 0) return;
738
+ const resourceKeys = [` all: ['${resource}'] as const,`];
739
+ const added = /* @__PURE__ */ new Set();
740
+ queryEndpoints.forEach(({ name, endpoint }) => {
741
+ const keyName = this.getEndpointKeyName(name);
742
+ if (added.has(keyName)) return;
743
+ const inferParams = this.inferNonNull(
744
+ `typeof apiConfig.endpoints.${name}.params`
745
+ );
746
+ const inferQuery = this.inferNonNull(
747
+ `typeof apiConfig.endpoints.${name}.query`
748
+ );
749
+ if (endpoint.params || endpoint.query) {
750
+ const params = [];
751
+ if (endpoint.params) params.push(`params?: ${inferParams}`);
752
+ if (endpoint.query) params.push(`query?: ${inferQuery}`);
753
+ resourceKeys.push(` ${keyName}: (${params.join(", ")}) =>
754
+ ['${resource}', '${keyName}', ${endpoint.params ? "params" : "undefined"}, ${endpoint.query ? "query" : "undefined"}] as const,`);
755
+ } else {
756
+ resourceKeys.push(
757
+ ` ${keyName}: () => ['${resource}', '${keyName}'] as const,`
758
+ );
759
+ }
760
+ added.add(keyName);
761
+ });
762
+ keys.push(` ${resource}: {
763
+ ${resourceKeys.join("\n")}
764
+ },`);
765
+ });
766
+ return keys.join("\n");
767
+ }
768
+ };
769
+ var QueryOptionsGenerator = class extends BaseGenerator {
770
+ async generate() {
771
+ const content = this.generateContent();
772
+ const outputPath = path6.join(
773
+ this.context.config.outputDir,
774
+ "query-options.ts"
775
+ );
776
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
777
+ await fs5.writeFile(outputPath, content, "utf-8");
778
+ }
779
+ generateContent() {
780
+ const content = `// Auto-generated query options
781
+ import { queryOptions } from '@tanstack/react-query';
782
+ import { apiClient } from './api-client';
783
+ import { queryKeys } from './query-keys';
784
+ import { z } from 'zod';
785
+ import { apiConfig } from '../config/endpoints';
786
+
787
+ ${this.generateQueryOptionsContent()}
788
+
789
+ export const apiQueryOptions = {
790
+ ${this.generateQueryOptionsExports()}
791
+ } as const;
792
+ `;
793
+ return content;
794
+ }
795
+ generateQueryOptionsContent() {
796
+ const groups = this.groupEndpointsByResource();
797
+ const options = [];
798
+ Object.entries(groups).forEach(([resource, endpoints]) => {
799
+ const queries = endpoints.filter(
800
+ ({ endpoint }) => endpoint.method === "GET"
801
+ );
802
+ if (queries.length === 0) return;
803
+ const resourceOptions = [];
804
+ queries.forEach(({ name, endpoint }) => {
805
+ const optionName = this.getEndpointKeyName(name);
806
+ const inferParams = this.inferNonNull(
807
+ `typeof apiConfig.endpoints.${name}.params`
808
+ );
809
+ const inferQuery = this.inferNonNull(
810
+ `typeof apiConfig.endpoints.${name}.query`
811
+ );
812
+ const inferResponse = this.inferNonNull(
813
+ `typeof apiConfig.endpoints.${name}.response`
814
+ );
815
+ const params = [];
816
+ let apiCall = "";
817
+ if (endpoint.params && endpoint.query) {
818
+ params.push(`params: ${inferParams}`, `filters?: ${inferQuery}`);
819
+ apiCall = `apiClient.${name}(params, filters)`;
820
+ } else if (endpoint.params) {
821
+ params.push(`params: ${inferParams}`);
822
+ apiCall = `apiClient.${name}(params)`;
823
+ } else if (endpoint.query) {
824
+ params.push(`filters?: ${inferQuery}`);
825
+ apiCall = `apiClient.${name}(filters)`;
826
+ } else {
827
+ apiCall = `apiClient.${name}()`;
828
+ }
829
+ const keyCall = this.generateQueryKeyCall(resource, name, endpoint);
830
+ resourceOptions.push(` ${optionName}: (${params.join(", ")}) =>
831
+ queryOptions({
832
+ queryKey: ${keyCall},
833
+ queryFn: (): Promise<${inferResponse}> => ${apiCall},
834
+ staleTime: 1000 * 60 * 5,
835
+ }),`);
836
+ });
837
+ options.push(
838
+ `const ${resource}QueryOptions = {
839
+ ${resourceOptions.join("\n")}
840
+ };
841
+ `
842
+ );
843
+ });
844
+ return options.join("\n");
845
+ }
846
+ generateQueryOptionsExports() {
847
+ const groups = this.groupEndpointsByResource();
848
+ const exports$1 = [];
849
+ Object.keys(groups).forEach((resource) => {
850
+ const hasQueries = groups[resource].some(
851
+ ({ endpoint }) => endpoint.method === "GET"
852
+ );
853
+ if (hasQueries) exports$1.push(` ${resource}: ${resource}QueryOptions,`);
854
+ });
855
+ return exports$1.join("\n");
856
+ }
544
857
  };
545
858
 
546
859
  // src/generators/index.ts
@@ -561,6 +874,8 @@ var CodeGenerator = class {
561
874
  generators.push(new ClientGenerator(this.context));
562
875
  }
563
876
  if (this.context.config.generateHooks) {
877
+ generators.push(new QueryKeysGenerator(this.context));
878
+ generators.push(new QueryOptionsGenerator(this.context));
564
879
  generators.push(new HooksGenerator(this.context));
565
880
  }
566
881
  if (this.context.config.generateServerActions && this.context.config.provider === "nextjs") {