@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/index.js CHANGED
@@ -184,9 +184,9 @@ var APIClient = class {
184
184
  })();
185
185
  return this.refreshPromise;
186
186
  }
187
- buildPath(path7, params) {
188
- if (!params) return path7;
189
- let finalPath = path7;
187
+ buildPath(path9, params) {
188
+ if (!params) return path9;
189
+ let finalPath = path9;
190
190
  Object.entries(params).forEach(([key, value]) => {
191
191
  finalPath = finalPath.replace(
192
192
  `:${key}`,
@@ -218,7 +218,7 @@ var APIClient = class {
218
218
  }
219
219
  async request(endpoint, params, query, body) {
220
220
  try {
221
- const path7 = this.buildPath(endpoint.path, params);
221
+ const path9 = this.buildPath(endpoint.path, params);
222
222
  const client = this.getClientForEndpoint(endpoint);
223
223
  const options = {
224
224
  method: endpoint.method
@@ -242,7 +242,7 @@ var APIClient = class {
242
242
  options.json = body;
243
243
  }
244
244
  }
245
- const response = await client(path7, options);
245
+ const response = await client(path9, options);
246
246
  const data = await response.json();
247
247
  if (endpoint.response) {
248
248
  return endpoint.response.parse(data);
@@ -384,6 +384,49 @@ var BaseGenerator = class {
384
384
  return `return apiClient.${name}();`;
385
385
  }
386
386
  }
387
+ inferNonNull(expr) {
388
+ return `z.infer<NonNullable<${expr}>>`;
389
+ }
390
+ toCamelCase(str) {
391
+ return str.toLowerCase().replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toLowerCase());
392
+ }
393
+ getResourceFromEndpoint(_name, endpoint) {
394
+ const tag = endpoint.tags?.find((t) => t !== "query" && t !== "mutation");
395
+ if (tag) return this.toCamelCase(tag);
396
+ const match = endpoint.path.match(/^\/([^/]+)/);
397
+ return match ? this.toCamelCase(match[1]) : "general";
398
+ }
399
+ groupEndpointsByResource() {
400
+ const groups = {};
401
+ Object.entries(this.context.apiConfig.endpoints).forEach(
402
+ ([name, endpoint]) => {
403
+ const res = this.getResourceFromEndpoint(name, endpoint);
404
+ if (!groups[res]) groups[res] = [];
405
+ groups[res].push({ name, endpoint });
406
+ }
407
+ );
408
+ return groups;
409
+ }
410
+ resourceHasQueryEndpoints(resource) {
411
+ return this.groupEndpointsByResource()[resource]?.some(
412
+ ({ endpoint }) => endpoint.method === "GET"
413
+ ) ?? false;
414
+ }
415
+ getEndpointKeyName(name) {
416
+ return name.startsWith("get") ? name[3].toLowerCase() + name.slice(4) : name;
417
+ }
418
+ generateQueryKeyCall(resource, name, endpoint) {
419
+ const key = this.getEndpointKeyName(name);
420
+ const args = [];
421
+ if (endpoint.params) args.push("params");
422
+ if (endpoint.query) args.push("filters");
423
+ return args.length ? `queryKeys.${resource}.${key}(${args.join(", ")})` : `queryKeys.${resource}.${key}()`;
424
+ }
425
+ hasQueryOptions() {
426
+ return Object.values(this.context.apiConfig.endpoints).some(
427
+ (e) => e.method === "GET"
428
+ );
429
+ }
387
430
  };
388
431
 
389
432
  // src/generators/hooks.ts
@@ -396,100 +439,132 @@ var HooksGenerator = class extends BaseGenerator {
396
439
  }
397
440
  generateContent() {
398
441
  const useClientDirective = this.context.config.options?.useClientDirective ?? true;
399
- const imports = `${useClientDirective ? "'use client';\n" : ""}
400
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
401
- import type {
402
- UseQueryOptions,
403
- UseMutationOptions,
404
- QueryKey
405
- } from '@tanstack/react-query';
406
- import { apiClient } from './client';
407
- import type {
408
- APIEndpoints,
409
- ExtractBody,
410
- ExtractParams,
411
- ExtractQuery,
412
- ExtractResponse
413
- } from './types';
442
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
443
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
444
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
445
+ const content = `${useClientDirective ? "'use client';\n" : ""}
446
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
447
+ import { apiClient } from "./client";
448
+ import { queryKeys } from "./query-keys";
449
+ import { apiQueryOptions } from "./query-options";
450
+ import { z } from "zod";
451
+ import { apiConfig } from "${relativePath}";
452
+
453
+ ${this.generateQueryHooks()}
454
+ ${this.generateMutationHooks()}
414
455
  `;
456
+ return content;
457
+ }
458
+ generateQueryHooks() {
415
459
  const hooks = [];
416
- Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
417
- if (this.isQueryEndpoint(endpoint)) {
418
- hooks.push(this.generateQueryHook(name, endpoint));
419
- } else {
420
- hooks.push(this.generateMutationHook(name, endpoint));
460
+ Object.entries(this.context.apiConfig.endpoints).forEach(
461
+ ([name, endpoint]) => {
462
+ if (endpoint.method === "GET")
463
+ hooks.push(this.generateQueryHook(name, endpoint));
421
464
  }
422
- });
423
- return imports + "\n" + hooks.join("\n\n");
465
+ );
466
+ return hooks.join("\n\n");
424
467
  }
425
468
  generateQueryHook(name, endpoint) {
426
- const hookPrefix = this.context.config.options?.hookPrefix || "use";
427
- const hookName = `${hookPrefix}${this.capitalize(name)}`;
428
- const signature = this.getEndpointSignature(name, endpoint);
469
+ const hookName = `use${this.capitalize(name)}`;
470
+ const resource = this.getResourceFromEndpoint(name, endpoint);
471
+ const optionName = this.getEndpointKeyName(name);
472
+ const inferParams = this.inferNonNull(
473
+ `typeof apiConfig.endpoints.${name}.params`
474
+ );
475
+ const inferQuery = this.inferNonNull(
476
+ `typeof apiConfig.endpoints.${name}.query`
477
+ );
478
+ const inferResponse = this.inferNonNull(
479
+ `typeof apiConfig.endpoints.${name}.response`
480
+ );
481
+ const params = [];
482
+ const optionParams = [];
429
483
  const queryTags = this.getQueryTags(endpoint);
430
- const paramDef = signature.hasParams ? `params: ${signature.paramType}` : "";
431
- const queryDef = signature.hasQuery ? `query?: ${signature.queryType}` : "";
432
- const optionsDef = `options?: Omit<UseQueryOptions<${signature.responseType}, Error, ${signature.responseType}, QueryKey>, 'queryKey' | 'queryFn'>`;
433
- const paramsList = [paramDef, queryDef, optionsDef].filter(Boolean).join(",\n ");
434
- const queryKeyParts = [
435
- ...queryTags.map((tag) => `'${tag}'`),
436
- signature.hasParams ? "params" : "undefined",
437
- signature.hasQuery ? "query" : "undefined"
438
- ];
439
- const clientCallArgs = [];
440
- if (signature.hasParams) clientCallArgs.push("params");
441
- if (signature.hasQuery) clientCallArgs.push("query");
484
+ if (endpoint.params) {
485
+ params.push(`params: ${inferParams}`);
486
+ optionParams.push("params");
487
+ }
488
+ if (endpoint.query) {
489
+ params.push(`filters?: ${inferQuery}`);
490
+ optionParams.push("filters");
491
+ }
492
+ params.push(`options?: {
493
+ enabled?: boolean;
494
+ select?: <TData = ${inferResponse}>(data: ${inferResponse}) => TData;
495
+ }`);
442
496
  return `/**
443
497
  * ${endpoint.description || `Query hook for ${name}`}
444
498
  * @tags ${queryTags.join(", ") || "none"}
445
499
  */
446
- export function ${hookName}(
447
- ${paramsList}
448
- ) {
500
+ export function ${hookName}(${params.join(",\n ")}) {
449
501
  return useQuery({
450
- queryKey: [${queryKeyParts.join(", ")}] as const,
451
- queryFn: () => apiClient.${name}(${clientCallArgs.join(", ")}),
502
+ ...apiQueryOptions.${resource}.${optionName}(${optionParams.join(", ")}),
452
503
  ...options,
453
504
  });
454
505
  }`;
455
506
  }
507
+ generateMutationHooks() {
508
+ const hooks = [];
509
+ Object.entries(this.context.apiConfig.endpoints).forEach(
510
+ ([name, endpoint]) => {
511
+ if (endpoint.method !== "GET")
512
+ hooks.push(this.generateMutationHook(name, endpoint));
513
+ }
514
+ );
515
+ return hooks.join("\n\n");
516
+ }
456
517
  generateMutationHook(name, endpoint) {
457
- const hookPrefix = this.context.config.options?.hookPrefix || "use";
458
- const hookName = `${hookPrefix}${this.capitalize(name)}`;
459
- const signature = this.getEndpointSignature(name, endpoint);
460
- const invalidationTags = this.getInvalidationTags(endpoint);
461
- let inputType = "void";
462
- if (signature.hasParams && signature.hasBody) {
463
- inputType = `{ params: ${signature.paramType}; body: ${signature.bodyType} }`;
464
- } else if (signature.hasParams) {
465
- inputType = signature.paramType;
466
- } else if (signature.hasBody) {
467
- inputType = signature.bodyType;
518
+ const hookName = `use${this.capitalize(name)}`;
519
+ const resource = this.getResourceFromEndpoint(name, endpoint);
520
+ const inferParams = this.inferNonNull(
521
+ `typeof apiConfig.endpoints.${name}.params`
522
+ );
523
+ const inferBody = this.inferNonNull(
524
+ `typeof apiConfig.endpoints.${name}.body`
525
+ );
526
+ const inferResponse = this.inferNonNull(
527
+ `typeof apiConfig.endpoints.${name}.response`
528
+ );
529
+ const resourceHasQueries = this.resourceHasQueryEndpoints(resource);
530
+ let inputType;
531
+ let fnBody;
532
+ if (endpoint.params && endpoint.body) {
533
+ inputType = `{ params: ${inferParams}; body: ${inferBody}; }`;
534
+ fnBody = `({ params, body }: ${inputType}) => apiClient.${name}(params, body)`;
535
+ } else if (endpoint.params) {
536
+ inputType = `${inferParams}`;
537
+ fnBody = `(params: ${inputType}) => apiClient.${name}(params)`;
538
+ } else if (endpoint.body) {
539
+ inputType = `${inferBody}`;
540
+ fnBody = `(body: ${inputType}) => apiClient.${name}(body)`;
541
+ } else {
542
+ inputType = "void";
543
+ fnBody = `() => apiClient.${name}()`;
468
544
  }
469
- const invalidationQueries = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` queryClient.invalidateQueries({ queryKey: ['${tag}'] });`).join("\n") : " // No automatic invalidations";
545
+ const invalidate = resourceHasQueries ? `queryClient.invalidateQueries({ queryKey: queryKeys.${resource}.all });` : "";
470
546
  return `/**
471
547
  * ${endpoint.description || `Mutation hook for ${name}`}
472
548
  * @tags ${endpoint.tags?.join(", ") || "none"}
473
549
  */
474
- export function ${hookName}(
475
- options?: Omit<UseMutationOptions<${signature.responseType}, Error, ${inputType}>, 'mutationFn'>
476
- ) {
477
- const queryClient = useQueryClient();
478
-
479
- return useMutation({
480
- mutationFn: ${inputType === "void" ? "() => {" : "(input) => {"}
481
- ${this.generateMutationCall(name, signature.hasParams, signature.hasBody)}
482
- },
483
- onSuccess: (data, variables, context) => {
484
- // Invalidate related queries
485
- ${invalidationQueries}
486
-
487
- // Call user's onSuccess if provided
488
- options?.onSuccess?.(data, variables, context);
489
- },
490
- ...options,
491
- });
492
- }`;
550
+ export function ${hookName}(options?: {
551
+ onSuccess?: (data: ${inferResponse}, variables: ${inputType}, context: unknown) => void;
552
+ onError?: (error: Error, variables: ${inputType}, context: unknown) => void;
553
+ onSettled?: (data: ${inferResponse} | undefined, error: Error | null, variables: ${inputType}, context: unknown) => void;
554
+ onMutate?: (variables: ${inputType}) => Promise<unknown> | unknown;
555
+ }) {
556
+ ${invalidate ? "const queryClient = useQueryClient();" : ""}
557
+ return useMutation({
558
+ mutationFn: ${fnBody},
559
+ onSuccess: (data, variables, context) => {
560
+ ${invalidate}
561
+ options?.onSuccess?.(data, variables, context);
562
+ },
563
+ onError: options?.onError,
564
+ onSettled: options?.onSettled,
565
+ onMutate: options?.onMutate,
566
+ });
567
+ }`;
493
568
  }
494
569
  };
495
570
  var ServerActionsGenerator = class extends BaseGenerator {
@@ -584,11 +659,13 @@ import type {
584
659
  } from './types';
585
660
  `;
586
661
  const queries = [];
587
- Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
588
- if (this.isQueryEndpoint(endpoint)) {
589
- queries.push(this.generateServerQuery(name, endpoint));
662
+ Object.entries(this.context.apiConfig.endpoints).forEach(
663
+ ([name, endpoint]) => {
664
+ if (this.isQueryEndpoint(endpoint)) {
665
+ queries.push(this.generateServerQuery(name, endpoint));
666
+ }
590
667
  }
591
- });
668
+ );
592
669
  return imports + "\n" + queries.join("\n\n");
593
670
  }
594
671
  generateServerQuery(name, endpoint) {
@@ -601,9 +678,7 @@ import type {
601
678
  const clientCallArgs = [];
602
679
  if (signature.hasParams) clientCallArgs.push("params");
603
680
  if (signature.hasQuery) clientCallArgs.push("query");
604
- const cacheKeyParts = [
605
- `'${name}'`
606
- ];
681
+ const cacheKeyParts = [`'${name}'`];
607
682
  if (signature.hasParams) cacheKeyParts.push("JSON.stringify(params)");
608
683
  if (signature.hasQuery) cacheKeyParts.push("JSON.stringify(query)");
609
684
  return `/**
@@ -632,13 +707,18 @@ var TypesGenerator = class extends BaseGenerator {
632
707
  await fs5.writeFile(outputPath, content, "utf-8");
633
708
  }
634
709
  generateContent() {
710
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
711
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
712
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
635
713
  return `// Auto-generated type definitions
636
714
  // Do not edit this file manually
637
715
 
638
716
  import type { z } from 'zod';
717
+ import { apiConfig } from '${relativePath}';
718
+
639
719
 
640
720
  // Re-export endpoint configuration types
641
- export type { APIConfig, APIEndpoint, HTTPMethod } from '@vietbus/api-codegen/config';
721
+ export type { APIConfig, APIEndpoint, HTTPMethod } from '@cushin/api-codegen/schema';
642
722
 
643
723
  /**
644
724
  * Type helper to extract params schema from an endpoint
@@ -671,13 +751,37 @@ export type ExtractResponse<T> = T extends { response: infer R extends z.ZodType
671
751
  /**
672
752
  * Import your API config to get typed endpoints
673
753
  *
674
- * @example
675
- * import { apiConfig } from './config/endpoints';
676
- * export type APIEndpoints = typeof apiConfig.endpoints;
677
754
  */
678
- export type APIEndpoints = Record<string, any>;
755
+ export type APIEndpoints = typeof apiConfig.endpoints;
756
+
757
+ ${this.generateEndpointTypes()}
679
758
  `;
680
759
  }
760
+ generateEndpointTypes() {
761
+ const types = [];
762
+ Object.entries(this.context.apiConfig.endpoints).forEach(
763
+ ([name, endpoint]) => {
764
+ const cap = this.capitalize(name);
765
+ if (endpoint.response)
766
+ types.push(
767
+ `export type ${cap}Response = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.response`)};`
768
+ );
769
+ if (endpoint.body)
770
+ types.push(
771
+ `export type ${cap}Input = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.body`)};`
772
+ );
773
+ if (endpoint.query)
774
+ types.push(
775
+ `export type ${cap}Query = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.query`)};`
776
+ );
777
+ if (endpoint.params)
778
+ types.push(
779
+ `export type ${cap}Params = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.params`)};`
780
+ );
781
+ }
782
+ );
783
+ return types.join("\n");
784
+ }
681
785
  };
682
786
  var ClientGenerator = class extends BaseGenerator {
683
787
  async generate() {
@@ -694,51 +798,62 @@ var ClientGenerator = class extends BaseGenerator {
694
798
  }
695
799
  async generateServerClientFile() {
696
800
  const content = this.generateServerClientContent();
697
- const outputPath = path6.join(this.context.config.outputDir, "server-client.ts");
801
+ const outputPath = path6.join(
802
+ this.context.config.outputDir,
803
+ "server-client.ts"
804
+ );
698
805
  await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
699
806
  await fs5.writeFile(outputPath, content, "utf-8");
700
807
  }
701
808
  generateClientContent() {
702
809
  const useClientDirective = this.context.config.options?.useClientDirective ?? true;
810
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
811
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
812
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
703
813
  return `${useClientDirective ? "'use client';\n" : ""}
704
814
  import { createAPIClient } from '@cushin/api-codegen/client';
705
815
  import type { AuthCallbacks } from '@cushin/api-codegen/client';
706
- import { apiConfig } from '../config/endpoints';
707
- import type { APIEndpoints } from './types';
816
+ import { apiConfig } from '${relativePath}';
817
+ import { z } from 'zod';
708
818
 
709
- // Type-safe API client methods
819
+ // Type the methods based on endpoints
710
820
  type APIClientMethods = {
711
- [K in keyof APIEndpoints]: APIEndpoints[K] extends {
821
+ [K in keyof typeof apiConfig.endpoints]: (typeof apiConfig.endpoints)[K] extends {
712
822
  method: infer M;
713
823
  params?: infer P;
714
824
  query?: infer Q;
715
825
  body?: infer B;
716
826
  response: infer R;
717
827
  }
718
- ? M extends 'GET'
719
- ? P extends { _type: any }
720
- ? Q extends { _type: any }
721
- ? (params: P['_type'], query?: Q['_type']) => Promise<R['_type']>
722
- : (params: P['_type']) => Promise<R['_type']>
723
- : Q extends { _type: any }
724
- ? (query?: Q['_type']) => Promise<R['_type']>
725
- : () => Promise<R['_type']>
726
- : P extends { _type: any }
727
- ? B extends { _type: any }
728
- ? (params: P['_type'], body: B['_type']) => Promise<R['_type']>
729
- : (params: P['_type']) => Promise<R['_type']>
730
- : B extends { _type: any }
731
- ? (body: B['_type']) => Promise<R['_type']>
732
- : () => Promise<R['_type']>
828
+ ? M extends "GET"
829
+ ? P extends z.ZodJSONSchema
830
+ ? Q extends z.ZodJSONSchema
831
+ ? (params: z.infer<P>, query?: z.infer<Q>) => Promise<z.infer<R>>
832
+ : (params: z.infer<P>) => Promise<z.infer<R>>
833
+ : Q extends z.ZodJSONSchema
834
+ ? (query?: z.infer<Q>) => Promise<z.infer<R>>
835
+ : () => Promise<z.infer<R>>
836
+ : P extends z.ZodJSONSchema
837
+ ? B extends z.ZodJSONSchema
838
+ ? (params: z.infer<P>, body: z.infer<B>) => Promise<z.infer<R>>
839
+ : (params: z.infer<P>) => Promise<z.infer<R>>
840
+ : B extends z.ZodJSONSchema
841
+ ? (body: z.infer<B>) => Promise<z.infer<R>>
842
+ : () => Promise<z.infer<R>>
733
843
  : never;
734
844
  };
735
845
 
846
+
736
847
  // Export singleton instance (will be initialized later)
737
- export let apiClient: APIClientMethods & {
848
+ export let baseClient: APIClientMethods & {
738
849
  refreshAuth: () => Promise<void>;
739
850
  updateAuthCallbacks: (callbacks: AuthCallbacks) => void;
740
851
  };
741
852
 
853
+ export const apiClient = {
854
+ ${this.generateApiClientMethods()}
855
+ };
856
+
742
857
  /**
743
858
  * Initialize API client with auth callbacks
744
859
  * Call this function in your auth provider setup
@@ -758,8 +873,8 @@ export let apiClient: APIClientMethods & {
758
873
  * initializeAPIClient(authCallbacks);
759
874
  */
760
875
  export const initializeAPIClient = (authCallbacks: AuthCallbacks) => {
761
- apiClient = createAPIClient(apiConfig, authCallbacks) as any;
762
- return apiClient;
876
+ baseClient = createAPIClient(apiConfig, authCallbacks) as any;
877
+ return baseClient;
763
878
  };
764
879
 
765
880
  // Export for custom usage
@@ -806,6 +921,204 @@ type APIClientMethods = {
806
921
  export const serverClient = createAPIClient(apiConfig) as APIClientMethods;
807
922
  `;
808
923
  }
924
+ generateApiClientMethods() {
925
+ const methods = [];
926
+ Object.entries(this.context.apiConfig.endpoints).forEach(
927
+ ([name, endpoint]) => {
928
+ const inferParams = this.inferNonNull(
929
+ `typeof apiConfig.endpoints.${name}.params`
930
+ );
931
+ const inferQuery = this.inferNonNull(
932
+ `typeof apiConfig.endpoints.${name}.query`
933
+ );
934
+ const inferBody = this.inferNonNull(
935
+ `typeof apiConfig.endpoints.${name}.body`
936
+ );
937
+ const inferResponse = this.inferNonNull(
938
+ `typeof apiConfig.endpoints.${name}.response`
939
+ );
940
+ if (endpoint.method === "GET") {
941
+ if (endpoint.params && endpoint.query) {
942
+ methods.push(` ${name}: (params: ${inferParams}, query?: ${inferQuery}): Promise<${inferResponse}> =>
943
+ (baseClient as any).${name}(params, query),`);
944
+ } else if (endpoint.params) {
945
+ methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
946
+ (baseClient as any).${name}(params),`);
947
+ } else if (endpoint.query) {
948
+ methods.push(` ${name}: (query?: ${inferQuery}): Promise<${inferResponse}> =>
949
+ (baseClient as any).${name}(query),`);
950
+ } else {
951
+ methods.push(` ${name}: (): Promise<${inferResponse}> =>
952
+ (baseClient as any).${name}(),`);
953
+ }
954
+ } else {
955
+ if (endpoint.params && endpoint.body) {
956
+ methods.push(` ${name}: (params: ${inferParams}, body: ${inferBody}): Promise<${inferResponse}> =>
957
+ (baseClient as any).${name}(params, body),`);
958
+ } else if (endpoint.params) {
959
+ methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
960
+ (baseClient as any).${name}(params),`);
961
+ } else if (endpoint.body) {
962
+ methods.push(` ${name}: (body: ${inferBody}): Promise<${inferResponse}> =>
963
+ (baseClient as any).${name}(body),`);
964
+ } else {
965
+ methods.push(` ${name}: (): Promise<${inferResponse}> =>
966
+ (baseClient as any).${name}(),`);
967
+ }
968
+ }
969
+ }
970
+ );
971
+ return methods.join("\n");
972
+ }
973
+ };
974
+ var QueryKeysGenerator = class extends BaseGenerator {
975
+ async generate() {
976
+ const content = this.generateContent();
977
+ const outputPath = path6.join(
978
+ this.context.config.outputDir,
979
+ "query-keys.ts"
980
+ );
981
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
982
+ await fs5.writeFile(outputPath, content, "utf-8");
983
+ }
984
+ generateContent() {
985
+ const content = `// Auto-generated query keys
986
+ import { z } from 'zod';
987
+ import { apiConfig } from '../config/endpoints';
988
+
989
+ export const queryKeys = {
990
+ ${this.generateQueryKeysContent()}
991
+ } as const;
992
+ `;
993
+ return content;
994
+ }
995
+ generateQueryKeysContent() {
996
+ const resourceGroups = this.groupEndpointsByResource();
997
+ const keys = [];
998
+ Object.entries(resourceGroups).forEach(([resource, endpoints]) => {
999
+ const queryEndpoints = endpoints.filter(
1000
+ ({ endpoint }) => endpoint.method === "GET"
1001
+ );
1002
+ if (queryEndpoints.length === 0) return;
1003
+ const resourceKeys = [` all: ['${resource}'] as const,`];
1004
+ const added = /* @__PURE__ */ new Set();
1005
+ queryEndpoints.forEach(({ name, endpoint }) => {
1006
+ const keyName = this.getEndpointKeyName(name);
1007
+ if (added.has(keyName)) return;
1008
+ const inferParams = this.inferNonNull(
1009
+ `typeof apiConfig.endpoints.${name}.params`
1010
+ );
1011
+ const inferQuery = this.inferNonNull(
1012
+ `typeof apiConfig.endpoints.${name}.query`
1013
+ );
1014
+ if (endpoint.params || endpoint.query) {
1015
+ const params = [];
1016
+ if (endpoint.params) params.push(`params?: ${inferParams}`);
1017
+ if (endpoint.query) params.push(`query?: ${inferQuery}`);
1018
+ resourceKeys.push(` ${keyName}: (${params.join(", ")}) =>
1019
+ ['${resource}', '${keyName}', ${endpoint.params ? "params" : "undefined"}, ${endpoint.query ? "query" : "undefined"}] as const,`);
1020
+ } else {
1021
+ resourceKeys.push(
1022
+ ` ${keyName}: () => ['${resource}', '${keyName}'] as const,`
1023
+ );
1024
+ }
1025
+ added.add(keyName);
1026
+ });
1027
+ keys.push(` ${resource}: {
1028
+ ${resourceKeys.join("\n")}
1029
+ },`);
1030
+ });
1031
+ return keys.join("\n");
1032
+ }
1033
+ };
1034
+ var QueryOptionsGenerator = class extends BaseGenerator {
1035
+ async generate() {
1036
+ const content = this.generateContent();
1037
+ const outputPath = path6.join(
1038
+ this.context.config.outputDir,
1039
+ "query-options.ts"
1040
+ );
1041
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
1042
+ await fs5.writeFile(outputPath, content, "utf-8");
1043
+ }
1044
+ generateContent() {
1045
+ const content = `// Auto-generated query options
1046
+ import { queryOptions } from '@tanstack/react-query';
1047
+ import { apiClient } from './api-client';
1048
+ import { queryKeys } from './query-keys';
1049
+ import { z } from 'zod';
1050
+ import { apiConfig } from '../config/endpoints';
1051
+
1052
+ ${this.generateQueryOptionsContent()}
1053
+
1054
+ export const apiQueryOptions = {
1055
+ ${this.generateQueryOptionsExports()}
1056
+ } as const;
1057
+ `;
1058
+ return content;
1059
+ }
1060
+ generateQueryOptionsContent() {
1061
+ const groups = this.groupEndpointsByResource();
1062
+ const options = [];
1063
+ Object.entries(groups).forEach(([resource, endpoints]) => {
1064
+ const queries = endpoints.filter(
1065
+ ({ endpoint }) => endpoint.method === "GET"
1066
+ );
1067
+ if (queries.length === 0) return;
1068
+ const resourceOptions = [];
1069
+ queries.forEach(({ name, endpoint }) => {
1070
+ const optionName = this.getEndpointKeyName(name);
1071
+ const inferParams = this.inferNonNull(
1072
+ `typeof apiConfig.endpoints.${name}.params`
1073
+ );
1074
+ const inferQuery = this.inferNonNull(
1075
+ `typeof apiConfig.endpoints.${name}.query`
1076
+ );
1077
+ const inferResponse = this.inferNonNull(
1078
+ `typeof apiConfig.endpoints.${name}.response`
1079
+ );
1080
+ const params = [];
1081
+ let apiCall = "";
1082
+ if (endpoint.params && endpoint.query) {
1083
+ params.push(`params: ${inferParams}`, `filters?: ${inferQuery}`);
1084
+ apiCall = `apiClient.${name}(params, filters)`;
1085
+ } else if (endpoint.params) {
1086
+ params.push(`params: ${inferParams}`);
1087
+ apiCall = `apiClient.${name}(params)`;
1088
+ } else if (endpoint.query) {
1089
+ params.push(`filters?: ${inferQuery}`);
1090
+ apiCall = `apiClient.${name}(filters)`;
1091
+ } else {
1092
+ apiCall = `apiClient.${name}()`;
1093
+ }
1094
+ const keyCall = this.generateQueryKeyCall(resource, name, endpoint);
1095
+ resourceOptions.push(` ${optionName}: (${params.join(", ")}) =>
1096
+ queryOptions({
1097
+ queryKey: ${keyCall},
1098
+ queryFn: (): Promise<${inferResponse}> => ${apiCall},
1099
+ staleTime: 1000 * 60 * 5,
1100
+ }),`);
1101
+ });
1102
+ options.push(
1103
+ `const ${resource}QueryOptions = {
1104
+ ${resourceOptions.join("\n")}
1105
+ };
1106
+ `
1107
+ );
1108
+ });
1109
+ return options.join("\n");
1110
+ }
1111
+ generateQueryOptionsExports() {
1112
+ const groups = this.groupEndpointsByResource();
1113
+ const exports$1 = [];
1114
+ Object.keys(groups).forEach((resource) => {
1115
+ const hasQueries = groups[resource].some(
1116
+ ({ endpoint }) => endpoint.method === "GET"
1117
+ );
1118
+ if (hasQueries) exports$1.push(` ${resource}: ${resource}QueryOptions,`);
1119
+ });
1120
+ return exports$1.join("\n");
1121
+ }
809
1122
  };
810
1123
 
811
1124
  // src/generators/index.ts
@@ -826,6 +1139,8 @@ var CodeGenerator = class {
826
1139
  generators.push(new ClientGenerator(this.context));
827
1140
  }
828
1141
  if (this.context.config.generateHooks) {
1142
+ generators.push(new QueryKeysGenerator(this.context));
1143
+ generators.push(new QueryOptionsGenerator(this.context));
829
1144
  generators.push(new HooksGenerator(this.context));
830
1145
  }
831
1146
  if (this.context.config.generateServerActions && this.context.config.provider === "nextjs") {