@cushin/api-codegen 1.0.3 → 1.0.4

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,7 +439,10 @@ 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" : ""}
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" : ""}
400
446
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
401
447
  import type {
402
448
  UseQueryOptions,
@@ -411,85 +457,126 @@ import type {
411
457
  ExtractQuery,
412
458
  ExtractResponse
413
459
  } from './types';
460
+ import { queryKeys } from './query-keys';
461
+ ${this.hasQueryOptions() ? "import { apiQueryOptions } from './query-options';" : ""}
462
+ import { z } from 'zod';
463
+ import { apiConfig } from '${relativePath}';
464
+
465
+ ${this.generateQueryHooks()}
466
+ ${this.generateMutationHooks()}
414
467
  `;
468
+ return content;
469
+ }
470
+ generateQueryHooks() {
415
471
  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));
472
+ Object.entries(this.context.apiConfig.endpoints).forEach(
473
+ ([name, endpoint]) => {
474
+ if (endpoint.method === "GET")
475
+ hooks.push(this.generateQueryHook(name, endpoint));
421
476
  }
422
- });
423
- return imports + "\n" + hooks.join("\n\n");
477
+ );
478
+ return hooks.join("\n\n");
424
479
  }
425
480
  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);
481
+ const hookName = `use${this.capitalize(name)}`;
482
+ const resource = this.getResourceFromEndpoint(name, endpoint);
483
+ const optionName = this.getEndpointKeyName(name);
484
+ const inferParams = this.inferNonNull(
485
+ `typeof apiConfig.endpoints.${name}.params`
486
+ );
487
+ const inferQuery = this.inferNonNull(
488
+ `typeof apiConfig.endpoints.${name}.query`
489
+ );
490
+ const inferResponse = this.inferNonNull(
491
+ `typeof apiConfig.endpoints.${name}.response`
492
+ );
493
+ const params = [];
494
+ const optionParams = [];
429
495
  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");
496
+ if (endpoint.params) {
497
+ params.push(`params: ${inferParams}`);
498
+ optionParams.push("params");
499
+ }
500
+ if (endpoint.query) {
501
+ params.push(`filters?: ${inferQuery}`);
502
+ optionParams.push("filters");
503
+ }
504
+ params.push(`options?: {
505
+ enabled?: boolean;
506
+ select?: <TData = ${inferResponse}>(data: ${inferResponse}) => TData;
507
+ }`);
442
508
  return `/**
443
509
  * ${endpoint.description || `Query hook for ${name}`}
444
510
  * @tags ${queryTags.join(", ") || "none"}
445
511
  */
446
- export function ${hookName}(
447
- ${paramsList}
448
- ) {
512
+ export function ${hookName}(${params.join(",\n ")}) {
449
513
  return useQuery({
450
- queryKey: [${queryKeyParts.join(", ")}] as const,
451
- queryFn: () => apiClient.${name}(${clientCallArgs.join(", ")}),
514
+ ...apiQueryOptions.${resource}.${optionName}(${optionParams.join(", ")}),
452
515
  ...options,
453
516
  });
454
517
  }`;
455
518
  }
519
+ generateMutationHooks() {
520
+ const hooks = [];
521
+ Object.entries(this.context.apiConfig.endpoints).forEach(
522
+ ([name, endpoint]) => {
523
+ if (endpoint.method !== "GET")
524
+ hooks.push(this.generateMutationHook(name, endpoint));
525
+ }
526
+ );
527
+ return hooks.join("\n\n");
528
+ }
456
529
  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;
530
+ const hookName = `use${this.capitalize(name)}`;
531
+ const resource = this.getResourceFromEndpoint(name, endpoint);
532
+ const inferParams = this.inferNonNull(
533
+ `typeof apiConfig.endpoints.${name}.params`
534
+ );
535
+ const inferBody = this.inferNonNull(
536
+ `typeof apiConfig.endpoints.${name}.body`
537
+ );
538
+ const inferResponse = this.inferNonNull(
539
+ `typeof apiConfig.endpoints.${name}.response`
540
+ );
541
+ const resourceHasQueries = this.resourceHasQueryEndpoints(resource);
542
+ let inputType;
543
+ let fnBody;
544
+ if (endpoint.params && endpoint.body) {
545
+ inputType = `{ params: ${inferParams}; body: ${inferBody}; }`;
546
+ fnBody = `({ params, body }: ${inputType}) => apiClient.${name}(params, body)`;
547
+ } else if (endpoint.params) {
548
+ inputType = `${inferParams}`;
549
+ fnBody = `(params: ${inputType}) => apiClient.${name}(params)`;
550
+ } else if (endpoint.body) {
551
+ inputType = `${inferBody}`;
552
+ fnBody = `(body: ${inputType}) => apiClient.${name}(body)`;
553
+ } else {
554
+ inputType = "void";
555
+ fnBody = `() => apiClient.${name}()`;
468
556
  }
469
- const invalidationQueries = invalidationTags.length > 0 ? invalidationTags.map((tag) => ` queryClient.invalidateQueries({ queryKey: ['${tag}'] });`).join("\n") : " // No automatic invalidations";
557
+ const invalidate = resourceHasQueries ? `queryClient.invalidateQueries({ queryKey: queryKeys.${resource}.all });` : "";
470
558
  return `/**
471
559
  * ${endpoint.description || `Mutation hook for ${name}`}
472
560
  * @tags ${endpoint.tags?.join(", ") || "none"}
473
561
  */
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
- }`;
562
+ export function ${hookName}(options?: {
563
+ onSuccess?: (data: ${inferResponse}, variables: ${inputType}, context: unknown) => void;
564
+ onError?: (error: Error, variables: ${inputType}, context: unknown) => void;
565
+ onSettled?: (data: ${inferResponse} | undefined, error: Error | null, variables: ${inputType}, context: unknown) => void;
566
+ onMutate?: (variables: ${inputType}) => Promise<unknown> | unknown;
567
+ }) {
568
+ ${invalidate ? "const queryClient = useQueryClient();" : ""}
569
+ return useMutation({
570
+ mutationFn: ${fnBody},
571
+ onSuccess: (data, variables, context) => {
572
+ ${invalidate}
573
+ options?.onSuccess?.(data, variables, context);
574
+ },
575
+ onError: options?.onError,
576
+ onSettled: options?.onSettled,
577
+ onMutate: options?.onMutate,
578
+ });
579
+ }`;
493
580
  }
494
581
  };
495
582
  var ServerActionsGenerator = class extends BaseGenerator {
@@ -584,11 +671,13 @@ import type {
584
671
  } from './types';
585
672
  `;
586
673
  const queries = [];
587
- Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
588
- if (this.isQueryEndpoint(endpoint)) {
589
- queries.push(this.generateServerQuery(name, endpoint));
674
+ Object.entries(this.context.apiConfig.endpoints).forEach(
675
+ ([name, endpoint]) => {
676
+ if (this.isQueryEndpoint(endpoint)) {
677
+ queries.push(this.generateServerQuery(name, endpoint));
678
+ }
590
679
  }
591
- });
680
+ );
592
681
  return imports + "\n" + queries.join("\n\n");
593
682
  }
594
683
  generateServerQuery(name, endpoint) {
@@ -601,9 +690,7 @@ import type {
601
690
  const clientCallArgs = [];
602
691
  if (signature.hasParams) clientCallArgs.push("params");
603
692
  if (signature.hasQuery) clientCallArgs.push("query");
604
- const cacheKeyParts = [
605
- `'${name}'`
606
- ];
693
+ const cacheKeyParts = [`'${name}'`];
607
694
  if (signature.hasParams) cacheKeyParts.push("JSON.stringify(params)");
608
695
  if (signature.hasQuery) cacheKeyParts.push("JSON.stringify(query)");
609
696
  return `/**
@@ -632,13 +719,18 @@ var TypesGenerator = class extends BaseGenerator {
632
719
  await fs5.writeFile(outputPath, content, "utf-8");
633
720
  }
634
721
  generateContent() {
722
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
723
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
724
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
635
725
  return `// Auto-generated type definitions
636
726
  // Do not edit this file manually
637
727
 
638
728
  import type { z } from 'zod';
729
+ import { apiConfig } from '${relativePath}';
730
+
639
731
 
640
732
  // Re-export endpoint configuration types
641
- export type { APIConfig, APIEndpoint, HTTPMethod } from '@vietbus/api-codegen/config';
733
+ export type { APIConfig, APIEndpoint, HTTPMethod } from '@cushin/api-codegen/schema';
642
734
 
643
735
  /**
644
736
  * Type helper to extract params schema from an endpoint
@@ -671,13 +763,35 @@ export type ExtractResponse<T> = T extends { response: infer R extends z.ZodType
671
763
  /**
672
764
  * Import your API config to get typed endpoints
673
765
  *
674
- * @example
675
- * import { apiConfig } from './config/endpoints';
676
- * export type APIEndpoints = typeof apiConfig.endpoints;
677
766
  */
678
- export type APIEndpoints = Record<string, any>;
767
+ export type APIEndpoints = typeof apiConfig.endpoints;
768
+
769
+ ${this.generateEndpointTypes()}
679
770
  `;
680
771
  }
772
+ generateEndpointTypes() {
773
+ const types = [];
774
+ Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
775
+ const cap = this.capitalize(name);
776
+ if (endpoint.response)
777
+ types.push(
778
+ `export type ${cap}Response = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.response`)};`
779
+ );
780
+ if (endpoint.body)
781
+ types.push(
782
+ `export type ${cap}Input = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.body`)};`
783
+ );
784
+ if (endpoint.query)
785
+ types.push(
786
+ `export type ${cap}Query = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.query`)};`
787
+ );
788
+ if (endpoint.params)
789
+ types.push(
790
+ `export type ${cap}Params = ${this.inferNonNull(`typeof apiConfig.endpoints.${name}.params`)};`
791
+ );
792
+ });
793
+ return types.join("\n");
794
+ }
681
795
  };
682
796
  var ClientGenerator = class extends BaseGenerator {
683
797
  async generate() {
@@ -700,11 +814,15 @@ var ClientGenerator = class extends BaseGenerator {
700
814
  }
701
815
  generateClientContent() {
702
816
  const useClientDirective = this.context.config.options?.useClientDirective ?? true;
817
+ const outputPath = path6.join(this.context.config.outputDir, "types.ts");
818
+ const endpointsPath = path6.join(this.context.config.endpointsPath);
819
+ const relativePath = path6.relative(path6.dirname(outputPath), endpointsPath).replace(/\\/g, "/");
703
820
  return `${useClientDirective ? "'use client';\n" : ""}
704
821
  import { createAPIClient } from '@cushin/api-codegen/client';
705
822
  import type { AuthCallbacks } from '@cushin/api-codegen/client';
706
- import { apiConfig } from '../config/endpoints';
823
+ import { apiConfig } from '${relativePath}';
707
824
  import type { APIEndpoints } from './types';
825
+ import { z } from 'zod';
708
826
 
709
827
  // Type-safe API client methods
710
828
  type APIClientMethods = {
@@ -734,11 +852,15 @@ type APIClientMethods = {
734
852
  };
735
853
 
736
854
  // Export singleton instance (will be initialized later)
737
- export let apiClient: APIClientMethods & {
855
+ export let baseClient: APIClientMethods & {
738
856
  refreshAuth: () => Promise<void>;
739
857
  updateAuthCallbacks: (callbacks: AuthCallbacks) => void;
740
858
  };
741
859
 
860
+ export const apiClient = {
861
+ ${this.generateApiClientMethods()}
862
+ };
863
+
742
864
  /**
743
865
  * Initialize API client with auth callbacks
744
866
  * Call this function in your auth provider setup
@@ -758,8 +880,8 @@ export let apiClient: APIClientMethods & {
758
880
  * initializeAPIClient(authCallbacks);
759
881
  */
760
882
  export const initializeAPIClient = (authCallbacks: AuthCallbacks) => {
761
- apiClient = createAPIClient(apiConfig, authCallbacks) as any;
762
- return apiClient;
883
+ baseClient = createAPIClient(apiConfig, authCallbacks) as any;
884
+ return baseClient;
763
885
  };
764
886
 
765
887
  // Export for custom usage
@@ -806,6 +928,202 @@ type APIClientMethods = {
806
928
  export const serverClient = createAPIClient(apiConfig) as APIClientMethods;
807
929
  `;
808
930
  }
931
+ generateApiClientMethods() {
932
+ const methods = [];
933
+ Object.entries(this.context.apiConfig.endpoints).forEach(([name, endpoint]) => {
934
+ const inferParams = this.inferNonNull(
935
+ `typeof apiConfig.endpoints.${name}.params`
936
+ );
937
+ const inferQuery = this.inferNonNull(
938
+ `typeof apiConfig.endpoints.${name}.query`
939
+ );
940
+ const inferBody = this.inferNonNull(
941
+ `typeof apiConfig.endpoints.${name}.body`
942
+ );
943
+ const inferResponse = this.inferNonNull(
944
+ `typeof apiConfig.endpoints.${name}.response`
945
+ );
946
+ if (endpoint.method === "GET") {
947
+ if (endpoint.params && endpoint.query) {
948
+ methods.push(` ${name}: (params: ${inferParams}, query?: ${inferQuery}): Promise<${inferResponse}> =>
949
+ (baseClient as any).${name}(params, query),`);
950
+ } else if (endpoint.params) {
951
+ methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
952
+ (baseClient as any).${name}(params),`);
953
+ } else if (endpoint.query) {
954
+ methods.push(` ${name}: (query?: ${inferQuery}): Promise<${inferResponse}> =>
955
+ (baseClient as any).${name}(query),`);
956
+ } else {
957
+ methods.push(` ${name}: (): Promise<${inferResponse}> =>
958
+ (baseClient as any).${name}(),`);
959
+ }
960
+ } else {
961
+ if (endpoint.params && endpoint.body) {
962
+ methods.push(` ${name}: (params: ${inferParams}, body: ${inferBody}): Promise<${inferResponse}> =>
963
+ (baseClient as any).${name}(params, body),`);
964
+ } else if (endpoint.params) {
965
+ methods.push(` ${name}: (params: ${inferParams}): Promise<${inferResponse}> =>
966
+ (baseClient as any).${name}(params),`);
967
+ } else if (endpoint.body) {
968
+ methods.push(` ${name}: (body: ${inferBody}): Promise<${inferResponse}> =>
969
+ (baseClient as any).${name}(body),`);
970
+ } else {
971
+ methods.push(` ${name}: (): Promise<${inferResponse}> =>
972
+ (baseClient as any).${name}(),`);
973
+ }
974
+ }
975
+ });
976
+ return methods.join("\n");
977
+ }
978
+ };
979
+ var QueryKeysGenerator = class extends BaseGenerator {
980
+ async generate() {
981
+ const content = this.generateContent();
982
+ const outputPath = path6.join(
983
+ this.context.config.outputDir,
984
+ "query-keys.ts"
985
+ );
986
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
987
+ await fs5.writeFile(outputPath, content, "utf-8");
988
+ }
989
+ generateContent() {
990
+ const content = `// Auto-generated query keys
991
+ import { z } from 'zod';
992
+ import { apiConfig } from '../config/endpoints';
993
+
994
+ export const queryKeys = {
995
+ ${this.generateQueryKeysContent()}
996
+ } as const;
997
+ `;
998
+ return content;
999
+ }
1000
+ generateQueryKeysContent() {
1001
+ const resourceGroups = this.groupEndpointsByResource();
1002
+ const keys = [];
1003
+ Object.entries(resourceGroups).forEach(([resource, endpoints]) => {
1004
+ const queryEndpoints = endpoints.filter(
1005
+ ({ endpoint }) => endpoint.method === "GET"
1006
+ );
1007
+ if (queryEndpoints.length === 0) return;
1008
+ const resourceKeys = [` all: ['${resource}'] as const,`];
1009
+ const added = /* @__PURE__ */ new Set();
1010
+ queryEndpoints.forEach(({ name, endpoint }) => {
1011
+ const keyName = this.getEndpointKeyName(name);
1012
+ if (added.has(keyName)) return;
1013
+ const inferParams = this.inferNonNull(
1014
+ `typeof apiConfig.endpoints.${name}.params`
1015
+ );
1016
+ const inferQuery = this.inferNonNull(
1017
+ `typeof apiConfig.endpoints.${name}.query`
1018
+ );
1019
+ if (endpoint.params || endpoint.query) {
1020
+ const params = [];
1021
+ if (endpoint.params) params.push(`params?: ${inferParams}`);
1022
+ if (endpoint.query) params.push(`query?: ${inferQuery}`);
1023
+ resourceKeys.push(` ${keyName}: (${params.join(", ")}) =>
1024
+ ['${resource}', '${keyName}', ${endpoint.params ? "params" : "undefined"}, ${endpoint.query ? "query" : "undefined"}] as const,`);
1025
+ } else {
1026
+ resourceKeys.push(
1027
+ ` ${keyName}: () => ['${resource}', '${keyName}'] as const,`
1028
+ );
1029
+ }
1030
+ added.add(keyName);
1031
+ });
1032
+ keys.push(` ${resource}: {
1033
+ ${resourceKeys.join("\n")}
1034
+ },`);
1035
+ });
1036
+ return keys.join("\n");
1037
+ }
1038
+ };
1039
+ var QueryOptionsGenerator = class extends BaseGenerator {
1040
+ async generate() {
1041
+ const content = this.generateContent();
1042
+ const outputPath = path6.join(
1043
+ this.context.config.outputDir,
1044
+ "query-options.ts"
1045
+ );
1046
+ await fs5.mkdir(path6.dirname(outputPath), { recursive: true });
1047
+ await fs5.writeFile(outputPath, content, "utf-8");
1048
+ }
1049
+ generateContent() {
1050
+ const content = `// Auto-generated query options
1051
+ import { queryOptions } from '@tanstack/react-query';
1052
+ import { apiClient } from './api-client';
1053
+ import { queryKeys } from './query-keys';
1054
+ import { z } from 'zod';
1055
+ import { apiConfig } from '../config/endpoints';
1056
+
1057
+ ${this.generateQueryOptionsContent()}
1058
+
1059
+ export const apiQueryOptions = {
1060
+ ${this.generateQueryOptionsExports()}
1061
+ } as const;
1062
+ `;
1063
+ return content;
1064
+ }
1065
+ generateQueryOptionsContent() {
1066
+ const groups = this.groupEndpointsByResource();
1067
+ const options = [];
1068
+ Object.entries(groups).forEach(([resource, endpoints]) => {
1069
+ const queries = endpoints.filter(
1070
+ ({ endpoint }) => endpoint.method === "GET"
1071
+ );
1072
+ if (queries.length === 0) return;
1073
+ const resourceOptions = [];
1074
+ queries.forEach(({ name, endpoint }) => {
1075
+ const optionName = this.getEndpointKeyName(name);
1076
+ const inferParams = this.inferNonNull(
1077
+ `typeof apiConfig.endpoints.${name}.params`
1078
+ );
1079
+ const inferQuery = this.inferNonNull(
1080
+ `typeof apiConfig.endpoints.${name}.query`
1081
+ );
1082
+ const inferResponse = this.inferNonNull(
1083
+ `typeof apiConfig.endpoints.${name}.response`
1084
+ );
1085
+ const params = [];
1086
+ let apiCall = "";
1087
+ if (endpoint.params && endpoint.query) {
1088
+ params.push(`params: ${inferParams}`, `filters?: ${inferQuery}`);
1089
+ apiCall = `apiClient.${name}(params, filters)`;
1090
+ } else if (endpoint.params) {
1091
+ params.push(`params: ${inferParams}`);
1092
+ apiCall = `apiClient.${name}(params)`;
1093
+ } else if (endpoint.query) {
1094
+ params.push(`filters?: ${inferQuery}`);
1095
+ apiCall = `apiClient.${name}(filters)`;
1096
+ } else {
1097
+ apiCall = `apiClient.${name}()`;
1098
+ }
1099
+ const keyCall = this.generateQueryKeyCall(resource, name, endpoint);
1100
+ resourceOptions.push(` ${optionName}: (${params.join(", ")}) =>
1101
+ queryOptions({
1102
+ queryKey: ${keyCall},
1103
+ queryFn: (): Promise<${inferResponse}> => ${apiCall},
1104
+ staleTime: 1000 * 60 * 5,
1105
+ }),`);
1106
+ });
1107
+ options.push(
1108
+ `const ${resource}QueryOptions = {
1109
+ ${resourceOptions.join("\n")}
1110
+ };
1111
+ `
1112
+ );
1113
+ });
1114
+ return options.join("\n");
1115
+ }
1116
+ generateQueryOptionsExports() {
1117
+ const groups = this.groupEndpointsByResource();
1118
+ const exports$1 = [];
1119
+ Object.keys(groups).forEach((resource) => {
1120
+ const hasQueries = groups[resource].some(
1121
+ ({ endpoint }) => endpoint.method === "GET"
1122
+ );
1123
+ if (hasQueries) exports$1.push(` ${resource}: ${resource}QueryOptions,`);
1124
+ });
1125
+ return exports$1.join("\n");
1126
+ }
809
1127
  };
810
1128
 
811
1129
  // src/generators/index.ts
@@ -826,6 +1144,8 @@ var CodeGenerator = class {
826
1144
  generators.push(new ClientGenerator(this.context));
827
1145
  }
828
1146
  if (this.context.config.generateHooks) {
1147
+ generators.push(new QueryKeysGenerator(this.context));
1148
+ generators.push(new QueryOptionsGenerator(this.context));
829
1149
  generators.push(new HooksGenerator(this.context));
830
1150
  }
831
1151
  if (this.context.config.generateServerActions && this.context.config.provider === "nextjs") {