@qualisero/openapi-endpoint 0.13.2 → 0.14.0

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
@@ -203,7 +203,7 @@ function addMissingOperationIds(openApiSpec, prefixToStrip = '/api') {
203
203
  });
204
204
  });
205
205
  }
206
- function parseOperationsFromSpec(openapiContent, excludePrefix = '_deprecated') {
206
+ function _parseOperationsFromSpec(openapiContent, excludePrefix = '_deprecated') {
207
207
  const openApiSpec = JSON.parse(openapiContent);
208
208
  if (!openApiSpec.paths) {
209
209
  throw new Error('Invalid OpenAPI spec: missing paths');
@@ -570,18 +570,18 @@ function generateApiEnumsContent(enums) {
570
570
  // Generate the generic enum helper utility
571
571
  const helperUtility = `/**
572
572
  * Generic utility for working with enums
573
- *
573
+ *
574
574
  * @example
575
575
  * import { EnumHelper, RequestedValuationType } from './api-enums'
576
- *
576
+ *
577
577
  * // Get all values
578
578
  * const allTypes = EnumHelper.values(RequestedValuationType)
579
- *
579
+ *
580
580
  * // Validate a value
581
581
  * if (EnumHelper.isValid(RequestedValuationType, userInput)) {
582
582
  * // TypeScript knows userInput is RequestedValuationType
583
583
  * }
584
- *
584
+ *
585
585
  * // Reverse lookup
586
586
  * const key = EnumHelper.getKey(RequestedValuationType, 'cat') // 'Cat'
587
587
  */
@@ -698,10 +698,10 @@ import type { components } from './openapi-types.js'
698
698
  /**
699
699
  * Type aliases for schema objects from the API spec.
700
700
  * These are references to components['schemas'] for convenient importing.
701
- *
701
+ *
702
702
  * @example
703
703
  * import type { Nuts, Address, BorrowerInfo } from './api-schemas'
704
- *
704
+ *
705
705
  * const nutsData: Nuts = { NUTS_ID: 'BE241', ... }
706
706
  */
707
707
  `;
@@ -731,235 +731,451 @@ async function generateApiSchemas(openapiContent, outputDir, _excludePrefix = '_
731
731
  console.log(`✅ Generated api-schemas file: ${outputPath}`);
732
732
  console.log(`📊 Found ${schemaCount} schemas`);
733
733
  }
734
- function generateApiOperationsContent(operationIds, operationInfoMap) {
735
- // Generate operationsBase dictionary
736
- const operationsBaseContent = operationIds
737
- .map((id) => {
738
- const info = operationInfoMap[id];
739
- return ` ${id}: {\n path: '${info.path}',\n method: HttpMethod.${info.method},\n },`;
740
- })
741
- .join('\n');
742
- const queryOperationIds = operationIds.filter((id) => {
743
- const method = operationInfoMap[id]?.method;
744
- return method === HttpMethod.GET || method === HttpMethod.HEAD || method === HttpMethod.OPTIONS;
745
- });
746
- const mutationOperationIds = operationIds.filter((id) => {
747
- const method = operationInfoMap[id]?.method;
748
- return (method === HttpMethod.POST ||
749
- method === HttpMethod.PUT ||
750
- method === HttpMethod.PATCH ||
751
- method === HttpMethod.DELETE);
752
- });
753
- // Generate filtered OperationId enums (source of truth)
754
- const queryOperationIdContent = queryOperationIds.map((id) => ` ${id}: '${id}' as const,`).join('\n');
755
- const mutationOperationIdContent = mutationOperationIds.map((id) => ` ${id}: '${id}' as const,`).join('\n');
756
- // Generate OpType namespace from BOTH lists
757
- const opTypeContent = [
758
- ...queryOperationIds.map((id) => ` export type ${id} = typeof QueryOperationId.${id}`),
759
- ...mutationOperationIds.map((id) => ` export type ${id} = typeof MutationOperationId.${id}`),
760
- ].join('\n');
761
- // Generate pre-computed type alias content for Phase 3B
762
- const queryParamsContent = queryOperationIds.map((id) => ` ${id}: ApiQueryParams<OpType.${id}>`).join('\n');
763
- const mutationParamsContent = mutationOperationIds.map((id) => ` ${id}: ApiPathParams<OpType.${id}>`).join('\n');
764
- const mutationBodyContent = mutationOperationIds.map((id) => ` ${id}: ApiRequest<OpType.${id}>`).join('\n');
765
- return `// Auto-generated from OpenAPI specification
766
- // Do not edit this file manually
734
+ // ============================================================================
735
+ // List path computation (ported from openapi-helpers.ts for code-gen time use)
736
+ // ============================================================================
737
+ const PLURAL_ES_SUFFIXES_CLI = ['s', 'x', 'z', 'ch', 'sh', 'o'];
738
+ function pluralizeResourceCli(name) {
739
+ if (name.endsWith('y'))
740
+ return name.slice(0, -1) + 'ies';
741
+ if (PLURAL_ES_SUFFIXES_CLI.some((s) => name.endsWith(s)))
742
+ return name + 'es';
743
+ return name + 's';
744
+ }
745
+ /**
746
+ * Computes the list path for a mutation operation (used for cache invalidation).
747
+ * Returns null if no matching list operation is found.
748
+ */
749
+ function computeListPath(operationId, opInfo, operationMap) {
750
+ const method = opInfo.method;
751
+ if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method))
752
+ return null;
753
+ const prefixes = {
754
+ [HttpMethod.POST]: 'create',
755
+ [HttpMethod.PUT]: 'update',
756
+ [HttpMethod.PATCH]: 'update',
757
+ [HttpMethod.DELETE]: 'delete',
758
+ };
759
+ const prefix = prefixes[method];
760
+ if (!prefix)
761
+ return null;
762
+ let resourceName = null;
763
+ if (operationId.startsWith(prefix)) {
764
+ const remaining = operationId.slice(prefix.length);
765
+ if (remaining.length > 0 && /^[A-Z]/.test(remaining))
766
+ resourceName = remaining;
767
+ }
768
+ if (resourceName) {
769
+ const tryList = (name) => {
770
+ const listId = `list${name}`;
771
+ if (listId in operationMap && operationMap[listId].method === HttpMethod.GET)
772
+ return operationMap[listId].path;
773
+ return null;
774
+ };
775
+ const found = tryList(resourceName) || tryList(pluralizeResourceCli(resourceName));
776
+ if (found)
777
+ return found;
778
+ }
779
+ // Fallback: strip last path param segment
780
+ const segments = opInfo.path.split('/').filter((s) => s.length > 0);
781
+ if (segments.length >= 2 && /^\{[^}]+\}$/.test(segments[segments.length - 1])) {
782
+ return '/' + segments.slice(0, -1).join('/') + '/';
783
+ }
784
+ return null;
785
+ }
786
+ // ============================================================================
787
+ // New generator: api-client.ts
788
+ // ============================================================================
789
+ /**
790
+ * Generate JSDoc comment for an operation function.
791
+ */
792
+ function _generateOperationJSDoc(operationId, method, apiPath) {
793
+ const methodUpper = method.toUpperCase();
794
+ const isQuery = ['GET', 'HEAD', 'OPTIONS'].includes(methodUpper);
795
+ const lines = ['/**', ` * ${operationId}`, ' * ', ` * ${methodUpper} ${apiPath}`];
796
+ if (isQuery) {
797
+ lines.push(' * ');
798
+ lines.push(' * @param pathParams - Path parameters (reactive)');
799
+ lines.push(' * @param options - Query options (enabled, staleTime, onLoad, etc.)');
800
+ lines.push(' * @returns Query result with data, isLoading, refetch(), etc.');
801
+ }
802
+ else {
803
+ lines.push(' * ');
804
+ lines.push(' * @param pathParams - Path parameters (reactive)');
805
+ lines.push(' * @param options - Mutation options (onSuccess, onError, invalidateOperations, etc.)');
806
+ lines.push(' * @returns Mutation helper with mutate() and mutateAsync() methods');
807
+ }
808
+ lines.push(' */');
809
+ return lines.join('\n');
810
+ }
811
+ function generateApiClientContent(operationMap) {
812
+ const ids = Object.keys(operationMap).sort();
813
+ const QUERY_HTTP = new Set(['GET', 'HEAD', 'OPTIONS']);
814
+ const isQuery = (id) => QUERY_HTTP.has(operationMap[id].method);
815
+ const hasPathParams = (id) => operationMap[id].path.includes('{');
816
+ // Registry for invalidateOperations support
817
+ const registryEntries = ids.map((id) => ` ${id}: { path: '${operationMap[id].path}' },`).join('\n');
818
+ // Generic factory helpers (4 patterns)
819
+ const helpers = `/**
820
+ * Generic query helper for operations without path parameters.
821
+ * @internal
822
+ */
823
+ function _queryNoParams<Op extends AllOps>(
824
+ base: _Config,
825
+ cfg: { path: string; method: HttpMethod; listPath: string | null },
826
+ enums: Record<string, unknown>,
827
+ ) {
828
+ type Response = ApiResponse<Op>
829
+ type QueryParams = ApiQueryParams<Op>
767
830
 
768
- import type { operations } from './openapi-types.js'
769
- import type {
770
- ApiResponse as ApiResponseBase,
771
- ApiResponseSafe as ApiResponseSafeBase,
772
- ApiRequest as ApiRequestBase,
773
- ApiPathParams as ApiPathParamsBase,
774
- ApiQueryParams as ApiQueryParamsBase,
775
- } from '@qualisero/openapi-endpoint'
831
+ const useQuery = (
832
+ options?: QueryOptions<Response, QueryParams>,
833
+ ): QueryReturn<Response, Record<string, never>> =>
834
+ useEndpointQuery<Response, Record<string, never>, QueryParams>(
835
+ { ...base, ...cfg },
836
+ undefined,
837
+ options,
838
+ )
776
839
 
777
- export enum HttpMethod {
778
- GET = 'GET',
779
- POST = 'POST',
780
- PUT = 'PUT',
781
- PATCH = 'PATCH',
782
- DELETE = 'DELETE',
783
- HEAD = 'HEAD',
784
- OPTIONS = 'OPTIONS',
785
- TRACE = 'TRACE',
840
+ return {
841
+ /**
842
+ * Query hook for this operation.
843
+ *
844
+ * Returns an object with:
845
+ * - \`data\`: The response data
846
+ * - \`isLoading\`: Whether the query is loading
847
+ * - \`error\`: Error object if the query failed
848
+ * - \`refetch\`: Function to manually trigger a refetch
849
+ * - \`isPending\`: Alias for isLoading
850
+ * - \`status\`: 'pending' | 'error' | 'success'
851
+ *
852
+ * @param options - Query options (enabled, refetchInterval, etc.)
853
+ * @returns Query result object
854
+ */
855
+ useQuery,
856
+ enums,
857
+ } as const
786
858
  }
787
859
 
788
- // Create the typed structure that combines operations with operation metadata
789
- // This ensures the debug method returns correct values and all operations are properly typed
790
- const operationsBase = {
791
- ${operationsBaseContent}
792
- } as const
860
+ /**
861
+ * Generic query helper for operations with path parameters.
862
+ * @internal
863
+ */
864
+ function _queryWithParams<Op extends AllOps>(
865
+ base: _Config,
866
+ cfg: { path: string; method: HttpMethod; listPath: string | null },
867
+ enums: Record<string, unknown>,
868
+ ) {
869
+ type PathParams = ApiPathParams<Op>
870
+ type PathParamsInput = ApiPathParamsInput<Op>
871
+ type Response = ApiResponse<Op>
872
+ type QueryParams = ApiQueryParams<Op>
793
873
 
794
- // Merge with operations type to maintain OpenAPI type information
795
- export const openApiOperations = operationsBase as typeof operationsBase & Pick<operations, keyof typeof operationsBase>
874
+ // Two-overload interface: non-function (exact via object-literal checking) +
875
+ // getter function (exact via NoExcessReturn constraint).
876
+ type _UseQuery = {
877
+ (
878
+ pathParams: PathParamsInput | Ref<PathParamsInput> | ComputedRef<PathParamsInput>,
879
+ options?: QueryOptions<Response, QueryParams>,
880
+ ): QueryReturn<Response, PathParams>
881
+ <F extends () => PathParamsInput>(
882
+ pathParams: NoExcessReturn<PathParamsInput, F>,
883
+ options?: QueryOptions<Response, QueryParams>,
884
+ ): QueryReturn<Response, PathParams>
885
+ }
796
886
 
797
- export type OpenApiOperations = typeof openApiOperations
887
+ const _impl = (
888
+ pathParams: ReactiveOr<PathParamsInput>,
889
+ options?: QueryOptions<Response, QueryParams>,
890
+ ): QueryReturn<Response, PathParams> =>
891
+ useEndpointQuery<Response, PathParams, QueryParams>(
892
+ { ...base, ...cfg },
893
+ pathParams as _PathParamsCast,
894
+ options,
895
+ )
798
896
 
799
- // Query operations only - use with useQuery() for better autocomplete
800
- export const QueryOperationId = {
801
- ${queryOperationIdContent}
802
- } satisfies Partial<Record<keyof typeof operationsBase, keyof typeof operationsBase>>
897
+ return {
898
+ /**
899
+ * Query hook for this operation.
900
+ *
901
+ * Returns an object with:
902
+ * - \`data\`: The response data
903
+ * - \`isLoading\`: Whether the query is loading
904
+ * - \`error\`: Error object if the query failed
905
+ * - \`refetch\`: Function to manually trigger a refetch
906
+ * - \`isPending\`: Alias for isLoading
907
+ * - \`status\`: 'pending' | 'error' | 'success'
908
+ *
909
+ * @param pathParams - Path parameters (object, ref, computed, or getter function)
910
+ * @param options - Query options (enabled, refetchInterval, etc.)
911
+ * @returns Query result object
912
+ */
913
+ useQuery: _impl as _UseQuery,
914
+ enums,
915
+ } as const
916
+ }
803
917
 
804
- export type QueryOperationId = keyof typeof QueryOperationId
918
+ /**
919
+ * Generic mutation helper for operations without path parameters.
920
+ * @internal
921
+ */
922
+ function _mutationNoParams<Op extends AllOps>(
923
+ base: _Config,
924
+ cfg: { path: string; method: HttpMethod; listPath: string | null },
925
+ enums: Record<string, unknown>,
926
+ ) {
927
+ type RequestBody = ApiRequest<Op>
928
+ type Response = ApiResponse<Op>
929
+ type QueryParams = ApiQueryParams<Op>
805
930
 
806
- // Mutation operations only - use with useMutation() for better autocomplete
807
- export const MutationOperationId = {
808
- ${mutationOperationIdContent}
809
- } satisfies Partial<Record<keyof typeof operationsBase, keyof typeof operationsBase>>
931
+ const useMutation = (
932
+ options?: MutationOptions<Response, Record<string, never>, RequestBody, QueryParams>,
933
+ ): MutationReturn<Response, Record<string, never>, RequestBody, QueryParams> =>
934
+ useEndpointMutation<Response, Record<string, never>, RequestBody, QueryParams>(
935
+ { ...base, ...cfg },
936
+ undefined,
937
+ options,
938
+ )
810
939
 
811
- export type MutationOperationId = keyof typeof MutationOperationId
940
+ return {
941
+ /**
942
+ * Mutation hook for this operation.
943
+ *
944
+ * Returns an object with:
945
+ * - \`mutate\`: Synchronous mutation function (returns void)
946
+ * - \`mutateAsync\`: Async mutation function (returns Promise)
947
+ * - \`data\`: The response data
948
+ * - \`isLoading\`: Whether the mutation is in progress
949
+ * - \`error\`: Error object if the mutation failed
950
+ * - \`isPending\`: Alias for isLoading
951
+ * - \`status\`: 'idle' | 'pending' | 'error' | 'success'
952
+ *
953
+ * @param options - Mutation options (onSuccess, onError, etc.)
954
+ * @returns Mutation result object
955
+ */
956
+ useMutation,
957
+ enums,
958
+ } as const
959
+ }
812
960
 
813
961
  /**
814
- * Union type of all operation IDs (queries and mutations).
815
- * Used for generic type constraints in helper types.
962
+ * Generic mutation helper for operations with path parameters.
816
963
  * @internal
817
964
  */
818
- export type AllOperationIds = QueryOperationId | MutationOperationId
965
+ function _mutationWithParams<Op extends AllOps>(
966
+ base: _Config,
967
+ cfg: { path: string; method: HttpMethod; listPath: string | null },
968
+ enums: Record<string, unknown>,
969
+ ) {
970
+ type PathParams = ApiPathParams<Op>
971
+ type PathParamsInput = ApiPathParamsInput<Op>
972
+ type RequestBody = ApiRequest<Op>
973
+ type Response = ApiResponse<Op>
974
+ type QueryParams = ApiQueryParams<Op>
819
975
 
820
- // ============================================================================
821
- // Type-safe API Helpers - Use OpType.XXX for type-safe access with intellisense
822
- // ============================================================================
976
+ // Two-overload interface: non-function (exact via object-literal checking) +
977
+ // getter function (exact via NoExcessReturn constraint).
978
+ type _UseMutation = {
979
+ (
980
+ pathParams: PathParamsInput | Ref<PathParamsInput> | ComputedRef<PathParamsInput>,
981
+ options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
982
+ ): MutationReturn<Response, PathParams, RequestBody, QueryParams>
983
+ <F extends () => PathParamsInput>(
984
+ pathParams: NoExcessReturn<PathParamsInput, F>,
985
+ options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
986
+ ): MutationReturn<Response, PathParams, RequestBody, QueryParams>
987
+ }
823
988
 
824
- /**
825
- * Response data type for an API operation.
826
- * All fields are REQUIRED - no null checks needed.
827
- * @example
828
- * type Response = ApiResponse<OpType.getPet>
829
- */
830
- export type ApiResponse<K extends AllOperationIds> = ApiResponseBase<OpenApiOperations, K>
989
+ const _impl = (
990
+ pathParams: ReactiveOr<PathParamsInput>,
991
+ options?: MutationOptions<Response, PathParams, RequestBody, QueryParams>,
992
+ ): MutationReturn<Response, PathParams, RequestBody, QueryParams> =>
993
+ useEndpointMutation<Response, PathParams, RequestBody, QueryParams>(
994
+ { ...base, ...cfg },
995
+ pathParams as _PathParamsCast,
996
+ options,
997
+ )
831
998
 
832
- /**
833
- * Response data type with safe typing for unreliable backends.
834
- * Only readonly properties are required; others may be undefined.
835
- * @example
836
- * type Response = ApiResponseSafe<OpType.getPet>
837
- */
838
- export type ApiResponseSafe<K extends AllOperationIds> = ApiResponseSafeBase<OpenApiOperations, K>
999
+ return {
1000
+ /**
1001
+ * Mutation hook for this operation.
1002
+ *
1003
+ * Returns an object with:
1004
+ * - \`mutate\`: Synchronous mutation function (returns void)
1005
+ * - \`mutateAsync\`: Async mutation function (returns Promise)
1006
+ * - \`data\`: The response data
1007
+ * - \`isLoading\`: Whether the mutation is in progress
1008
+ * - \`error\`: Error object if the mutation failed
1009
+ * - \`isPending\`: Alias for isLoading
1010
+ * - \`status\`: 'idle' | 'pending' | 'error' | 'success'
1011
+ *
1012
+ * @param pathParams - Path parameters (object, ref, computed, or getter function)
1013
+ * @param options - Mutation options (onSuccess, onError, etc.)
1014
+ * @returns Mutation result object
1015
+ */
1016
+ useMutation: _impl as _UseMutation,
1017
+ enums,
1018
+ } as const
1019
+ }`;
1020
+ // createApiClient factory with operation calls
1021
+ const factoryCalls = ids
1022
+ .map((id) => {
1023
+ const op = operationMap[id];
1024
+ const { path: apiPath, method } = op;
1025
+ const listPath = computeListPath(id, op, operationMap);
1026
+ const listPathStr = listPath ? `'${listPath}'` : 'null';
1027
+ const query = isQuery(id);
1028
+ const withParams = hasPathParams(id);
1029
+ const cfg = `{ path: '${apiPath}', method: HttpMethod.${method}, listPath: ${listPathStr} }`;
1030
+ const helper = query
1031
+ ? withParams
1032
+ ? '_queryWithParams'
1033
+ : '_queryNoParams'
1034
+ : withParams
1035
+ ? '_mutationWithParams'
1036
+ : '_mutationNoParams';
1037
+ // Build JSDoc comment
1038
+ const docLines = [];
1039
+ // Summary/description
1040
+ if (op.summary) {
1041
+ docLines.push(op.summary);
1042
+ }
1043
+ if (op.description && op.description !== op.summary) {
1044
+ docLines.push(op.description);
1045
+ }
1046
+ // Path parameters
1047
+ if (op.pathParams && op.pathParams.length > 0) {
1048
+ const paramList = op.pathParams.map((p) => `${p.name}: ${p.type}`).join(', ');
1049
+ docLines.push(`@param pathParams - { ${paramList} }`);
1050
+ }
1051
+ // Request body
1052
+ if (op.requestBodySchema) {
1053
+ docLines.push(`@param body - Request body type: ${op.requestBodySchema}`);
1054
+ }
1055
+ // Response
1056
+ if (op.responseSchema) {
1057
+ docLines.push(`@returns Response type: ${op.responseSchema}`);
1058
+ }
1059
+ const jsDoc = docLines.length > 0 ? `\n /**\n * ${docLines.join('\n * ')}\n */` : '';
1060
+ return `${jsDoc}\n ${id}: ${helper}<'${id}'>(base, ${cfg}, ${id}_enums),`;
1061
+ })
1062
+ .join('');
1063
+ // Enum imports
1064
+ const enumImports = ids.map((id) => ` ${id}_enums,`).join('\n');
1065
+ // Type alias for AllOps
1066
+ const allOpsType = `type AllOps = keyof operations`;
1067
+ return `// Auto-generated from OpenAPI specification - do not edit manually
1068
+ // Use \`createApiClient\` to instantiate a fully-typed API client.
839
1069
 
840
- /**
841
- * Request body type for a mutation operation.
842
- * @example
843
- * type Request = ApiRequest<OpType.createPet>
844
- */
845
- export type ApiRequest<K extends AllOperationIds> = ApiRequestBase<OpenApiOperations, K>
1070
+ import type { AxiosInstance } from 'axios'
1071
+ import type { Ref, ComputedRef } from 'vue'
1072
+ import {
1073
+ useEndpointQuery,
1074
+ useEndpointMutation,
1075
+ defaultQueryClient,
1076
+ HttpMethod,
1077
+ type QueryOptions,
1078
+ type MutationOptions,
1079
+ type QueryReturn,
1080
+ type MutationReturn,
1081
+ type ReactiveOr,
1082
+ type NoExcessReturn,
1083
+ type QueryClientLike,
1084
+ type MaybeRefOrGetter,
1085
+ } from '@qualisero/openapi-endpoint'
846
1086
 
847
- /**
848
- * Path parameters type for an operation.
849
- * @example
850
- * type Params = ApiPathParams<OpType.getPet>
851
- */
852
- export type ApiPathParams<K extends AllOperationIds> = ApiPathParamsBase<OpenApiOperations, K>
1087
+ import type {
1088
+ ApiResponse,
1089
+ ApiRequest,
1090
+ ApiPathParams,
1091
+ ApiPathParamsInput,
1092
+ ApiQueryParams,
1093
+ operations,
1094
+ } from './api-operations.js'
853
1095
 
854
- /**
855
- * Query parameters type for an operation.
856
- * @example
857
- * type Params = ApiQueryParams<OpType.listPets>
858
- */
859
- export type ApiQueryParams<K extends AllOperationIds> = ApiQueryParamsBase<OpenApiOperations, K>
1096
+ import {
1097
+ ${enumImports}
1098
+ } from './api-operations.js'
860
1099
 
861
1100
  // ============================================================================
862
- // OpType namespace - enables dot notation: ApiResponse<OpType.getPet>
1101
+ // Operations registry (for invalidateOperations support)
863
1102
  // ============================================================================
864
1103
 
865
- /**
866
- * Namespace that mirrors operation IDs as types.
867
- * Enables dot notation syntax: ApiResponse<OpType.getPet>
868
- *
869
- * This is the idiomatic TypeScript pattern for enabling dot notation
870
- * on type-level properties. The namespace is preferred over type aliases
871
- * because it allows \`OpType.getPet\` instead of \`OpType['getPet']\`.
872
- *
873
- * @example
874
- * type Response = ApiResponse<OpType.getPet>
875
- * type Request = ApiRequest<OpType.createPet>
876
- */
877
- // eslint-disable-next-line @typescript-eslint/no-namespace
878
- export namespace OpType {
879
- ${opTypeContent}
880
- }
1104
+ const _registry = {
1105
+ ${registryEntries}
1106
+ } as const
881
1107
 
882
1108
  // ============================================================================
883
- // Pre-Computed Type Aliases - For easier DX and clearer intent
1109
+ // Internal config type
884
1110
  // ============================================================================
885
1111
 
886
- /**
887
- * Query parameters for each query operation.
888
- *
889
- * Use this to get autocomplete on query parameter names and types.
890
- *
891
- * @example
892
- * \`\`\`typescript
893
- * const params: QueryParams['listPets'] = { limit: 10, status: 'available' }
894
- * const query = api.useQuery(QueryOperationId.listPets, { queryParams: params })
895
- * \`\`\`
896
- */
897
- export type QueryParams = {
898
- ${queryParamsContent}
1112
+ type _Config = {
1113
+ axios: AxiosInstance
1114
+ queryClient: QueryClientLike
1115
+ operationsRegistry: typeof _registry
899
1116
  }
900
1117
 
901
- /**
902
- * Path parameters for each mutation operation.
903
- *
904
- * Use this to get autocomplete on path parameter names and types.
905
- *
906
- * @example
907
- * \`\`\`typescript
908
- * const params: MutationParams['updatePet'] = { petId: '123' }
909
- * const mutation = api.useMutation(MutationOperationId.updatePet, params)
910
- * \`\`\`
911
- */
912
- export type MutationParams = {
913
- ${mutationParamsContent}
914
- }
1118
+ // ============================================================================
1119
+ // Type alias for path params cast (avoids repetition)
1120
+ // ============================================================================
915
1121
 
916
- /**
917
- * Request body for each mutation operation.
918
- *
919
- * Use this to get autocomplete on request body properties and types.
920
- *
921
- * @example
922
- * \`\`\`typescript
923
- * const body: MutationBody['createPet'] = { name: 'Fluffy', species: 'cat' }
924
- * await createPet.mutateAsync({ data: body })
925
- * \`\`\`
926
- */
927
- export type MutationBody = {
928
- ${mutationBodyContent}
929
- }
1122
+ type _PathParamsCast = MaybeRefOrGetter<Record<string, string | number | undefined> | null | undefined>
930
1123
 
931
1124
  // ============================================================================
932
- // LEGACY: OperationId (auto-derived from union for backward compatibility)
1125
+ // Type alias for all operations
933
1126
  // ============================================================================
934
- // Use QueryOperationId or MutationOperationId directly for better type safety
935
1127
 
936
- /**
937
- * @deprecated Use QueryOperationId or MutationOperationId instead.
938
- * Auto-derived from their union for backward compatibility.
939
- */
940
- export type OperationId = AllOperationIds
1128
+ ${allOpsType}
1129
+
1130
+ // ============================================================================
1131
+ // Shared generic factory helpers (4 patterns)
1132
+ // ============================================================================
1133
+
1134
+ ${helpers}
1135
+
1136
+ // ============================================================================
1137
+ // Public API client factory
1138
+ // ============================================================================
941
1139
 
942
1140
  /**
943
- * @deprecated Use QueryOperationId or MutationOperationId instead.
944
- * Auto-derived from their union for backward compatibility.
1141
+ * Create a fully-typed API client.
1142
+ *
1143
+ * Each operation in the spec is a property of the returned object:
1144
+ * - GET/HEAD/OPTIONS → \`api.opName.useQuery(...)\`
1145
+ * - POST/PUT/PATCH/DELETE → \`api.opName.useMutation(...)\`
1146
+ * - All operations → \`api.opName.enums.fieldName.Value\`
1147
+ *
1148
+ * @example
1149
+ * \`\`\`ts
1150
+ * import { createApiClient } from './generated/api-client'
1151
+ * import axios from 'axios'
1152
+ *
1153
+ * const api = createApiClient(axios.create({ baseURL: '/api' }))
1154
+ *
1155
+ * // In a Vue component:
1156
+ * const { data: pets } = api.listPets.useQuery()
1157
+ * const create = api.createPet.useMutation()
1158
+ * create.mutate({ data: { name: 'Fluffy' } })
1159
+ * \`\`\`
945
1160
  */
946
- export const OperationId = {
947
- ...QueryOperationId,
948
- ...MutationOperationId,
949
- } satisfies Record<AllOperationIds, AllOperationIds>
1161
+ export function createApiClient(axios: AxiosInstance, queryClient: QueryClientLike = defaultQueryClient) {
1162
+ const base: _Config = { axios, queryClient, operationsRegistry: _registry }
1163
+ return {
1164
+ ${factoryCalls}
1165
+ } as const
1166
+ }
1167
+
1168
+ /** The fully-typed API client instance type. */
1169
+ export type ApiClient = ReturnType<typeof createApiClient>
950
1170
  `;
951
1171
  }
952
- async function generateApiOperations(openapiContent, outputDir, excludePrefix = '_deprecated') {
953
- console.log('🔨 Generating openapi-typed-operations.ts file...');
954
- const { operationIds, operationInfoMap } = parseOperationsFromSpec(openapiContent, excludePrefix);
955
- // Generate TypeScript content
956
- const tsContent = generateApiOperationsContent(operationIds, operationInfoMap);
957
- // Write to output file
958
- const outputPath = path.join(outputDir, 'openapi-typed-operations.ts');
959
- fs.writeFileSync(outputPath, tsContent);
960
- console.log(`✅ Generated openapi-typed-operations file: ${outputPath}`);
961
- console.log(`📊 Found ${operationIds.length} operations`);
1172
+ async function generateApiClientFile(openApiSpec, outputDir, excludePrefix) {
1173
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1174
+ const content = generateApiClientContent(operationMap);
1175
+ fs.writeFileSync(path.join(outputDir, 'api-client.ts'), content);
1176
+ console.log(`✅ Generated api-client.ts (${Object.keys(operationMap).length} operations)`);
962
1177
  }
1178
+ // ============================================================================
963
1179
  function printUsage() {
964
1180
  console.log(`
965
1181
  Usage: npx @qualisero/openapi-endpoint <openapi-input> <output-directory> [options]
@@ -981,12 +1197,337 @@ Examples:
981
1197
  npx @qualisero/openapi-endpoint ./api.json ./src/gen --no-exclude
982
1198
 
983
1199
  This command will generate:
984
- - openapi-types.ts (TypeScript types from OpenAPI spec)
985
- - openapi-typed-operations.ts (Operation IDs and info for use with this library)
986
- - api-enums.ts (Type-safe enum objects from OpenAPI spec)
987
- - api-schemas.ts (Type aliases for schema objects from OpenAPI spec)
1200
+ - openapi-types.ts (TypeScript types from OpenAPI spec)
1201
+ - api-client.ts (Fully-typed createApiClient factory main file to use)
1202
+ - api-operations.ts (Operations map + type aliases)
1203
+ - api-types.ts (Types namespace for type-only access)
1204
+ - api-enums.ts (Type-safe enum objects from OpenAPI spec)
1205
+ - api-schemas.ts (Type aliases for schema objects from OpenAPI spec)
988
1206
  `);
989
1207
  }
1208
+ // ============================================================================
1209
+ // New helper functions for operation-named API
1210
+ // ============================================================================
1211
+ /**
1212
+ * Parses an already-loaded OpenAPISpec into a map of operationId → OperationInfo.
1213
+ * @param openApiSpec The parsed OpenAPI spec object
1214
+ * @param excludePrefix Operations with this prefix are excluded
1215
+ * @returns Map of operation ID to { path, method }
1216
+ */
1217
+ function buildOperationMap(openApiSpec, excludePrefix) {
1218
+ const map = {};
1219
+ for (const [pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
1220
+ for (const [method, rawOp] of Object.entries(pathItem)) {
1221
+ if (!HTTP_METHODS.includes(method))
1222
+ continue;
1223
+ const op = rawOp;
1224
+ if (!op.operationId)
1225
+ continue;
1226
+ if (excludePrefix && op.operationId.startsWith(excludePrefix))
1227
+ continue;
1228
+ // Extract path and query parameters
1229
+ const pathParams = [];
1230
+ const queryParams = [];
1231
+ if (op.parameters) {
1232
+ for (const param of op.parameters) {
1233
+ const type = param.schema?.type || 'string';
1234
+ if (param.in === 'path') {
1235
+ pathParams.push({ name: param.name, type });
1236
+ }
1237
+ else if (param.in === 'query') {
1238
+ queryParams.push({ name: param.name, type });
1239
+ }
1240
+ }
1241
+ }
1242
+ // Extract request body schema
1243
+ let requestBodySchema;
1244
+ const reqBodyRef = op.requestBody?.content?.['application/json']?.schema?.$ref;
1245
+ if (reqBodyRef) {
1246
+ requestBodySchema = reqBodyRef.split('/').pop();
1247
+ }
1248
+ // Extract response schema (from 200/201 responses)
1249
+ let responseSchema;
1250
+ if (op.responses) {
1251
+ for (const statusCode of ['200', '201']) {
1252
+ const resRef = op.responses[statusCode]?.content?.['application/json']?.schema?.$ref;
1253
+ if (resRef) {
1254
+ responseSchema = resRef.split('/').pop();
1255
+ break;
1256
+ }
1257
+ }
1258
+ }
1259
+ map[op.operationId] = {
1260
+ path: pathUrl,
1261
+ method: method.toUpperCase(),
1262
+ summary: op.summary,
1263
+ description: op.description,
1264
+ pathParams: pathParams.length > 0 ? pathParams : undefined,
1265
+ queryParams: queryParams.length > 0 ? queryParams : undefined,
1266
+ requestBodySchema,
1267
+ responseSchema,
1268
+ };
1269
+ }
1270
+ }
1271
+ return map;
1272
+ }
1273
+ /**
1274
+ * Converts an OpenAPI enum array to `{ MemberName: 'value' }`.
1275
+ * @param values Enum values (may include null)
1276
+ * @returns Object with PascalCase keys and string literal values
1277
+ */
1278
+ function enumArrayToObject(values) {
1279
+ const obj = {};
1280
+ for (const v of values) {
1281
+ if (v === null)
1282
+ continue;
1283
+ obj[toEnumMemberName(v)] = String(v);
1284
+ }
1285
+ return obj;
1286
+ }
1287
+ /**
1288
+ * For each operation, extract enum fields from:
1289
+ * 1. Request body object properties (direct `enum` or `$ref` to an enum schema)
1290
+ * 2. Query and path parameters with `enum`
1291
+ * @param openApiSpec The parsed OpenAPI spec
1292
+ * @param operationMap Map from buildOperationMap
1293
+ * @returns operationId → { fieldName → { MemberName: 'value' } }
1294
+ */
1295
+ function buildOperationEnums(openApiSpec, operationMap) {
1296
+ // Schema-level enum lookup: schemaName → { MemberName: value }
1297
+ const schemaEnumLookup = {};
1298
+ if (openApiSpec.components?.schemas) {
1299
+ for (const [schemaName, schema] of Object.entries(openApiSpec.components.schemas)) {
1300
+ if (schema.enum) {
1301
+ schemaEnumLookup[schemaName] = enumArrayToObject(schema.enum);
1302
+ }
1303
+ }
1304
+ }
1305
+ function resolveEnums(schema) {
1306
+ if (schema.enum)
1307
+ return enumArrayToObject(schema.enum);
1308
+ if (typeof schema.$ref === 'string') {
1309
+ const name = schema.$ref.split('/').pop();
1310
+ return schemaEnumLookup[name] ?? null;
1311
+ }
1312
+ return null;
1313
+ }
1314
+ const result = {};
1315
+ for (const [_pathUrl, pathItem] of Object.entries(openApiSpec.paths)) {
1316
+ for (const [method, rawOp] of Object.entries(pathItem)) {
1317
+ if (!HTTP_METHODS.includes(method))
1318
+ continue;
1319
+ const op = rawOp;
1320
+ if (!op.operationId || !(op.operationId in operationMap))
1321
+ continue;
1322
+ const fields = {};
1323
+ // Request body properties
1324
+ const bodyProps = op.requestBody?.content?.['application/json']?.schema?.properties;
1325
+ if (bodyProps) {
1326
+ for (const [fieldName, fieldSchema] of Object.entries(bodyProps)) {
1327
+ const resolved = resolveEnums(fieldSchema);
1328
+ if (resolved)
1329
+ fields[fieldName] = resolved;
1330
+ }
1331
+ }
1332
+ // Query + path parameters
1333
+ for (const param of op.parameters ?? []) {
1334
+ if (param.schema) {
1335
+ const resolved = resolveEnums(param.schema);
1336
+ if (resolved)
1337
+ fields[param.name] = resolved;
1338
+ }
1339
+ }
1340
+ result[op.operationId] = fields;
1341
+ }
1342
+ }
1343
+ return result;
1344
+ }
1345
+ // ============================================================================
1346
+ // New generators: api-operations.ts
1347
+ // ============================================================================
1348
+ /**
1349
+ * Generates the content for `api-operations.ts` file.
1350
+ * @param operationMap The operation info map
1351
+ * @param opEnums Per-operation enum fields
1352
+ * @param schemaEnumNames Names from api-enums.ts to re-export
1353
+ * @returns Generated TypeScript file content
1354
+ */
1355
+ function generateApiOperationsContent(operationMap, opEnums, schemaEnumNames) {
1356
+ const ids = Object.keys(operationMap).sort();
1357
+ const _queryIds = ids.filter((id) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method));
1358
+ const _mutationIds = ids.filter((id) => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(operationMap[id].method));
1359
+ // Per-operation enum consts
1360
+ const enumConsts = ids
1361
+ .map((id) => {
1362
+ const fields = opEnums[id] ?? {};
1363
+ const body = Object.entries(fields)
1364
+ .map(([field, vals]) => {
1365
+ const members = Object.entries(vals)
1366
+ .map(([k, v]) => ` ${k}: ${JSON.stringify(v)} as const,`)
1367
+ .join('\n');
1368
+ return ` ${field}: {\n${members}\n } as const,`;
1369
+ })
1370
+ .join('\n');
1371
+ return `export const ${id}_enums = {\n${body}\n} as const`;
1372
+ })
1373
+ .join('\n\n');
1374
+ // Operations map
1375
+ const opEntries = ids
1376
+ .map((id) => ` ${id}: { path: '${operationMap[id].path}', method: HttpMethod.${operationMap[id].method} },`)
1377
+ .join('\n');
1378
+ // Type helpers — now use openapi-typescript `operations` directly (not OpenApiOperations)
1379
+ const typeHelpers = `
1380
+ type AllOps = keyof operations
1381
+
1382
+ /** Response data type for an operation (all fields required). */
1383
+ export type ApiResponse<K extends AllOps> = _ApiResponse<operations, K>
1384
+ /** Response data type - only \`readonly\` fields required. */
1385
+ export type ApiResponseSafe<K extends AllOps> = _ApiResponseSafe<operations, K>
1386
+ /** Request body type. */
1387
+ export type ApiRequest<K extends AllOps> = _ApiRequest<operations, K>
1388
+ /** Path parameters type. */
1389
+ export type ApiPathParams<K extends AllOps> = _ApiPathParams<operations, K>
1390
+ /** Path parameters input type (allows undefined values for reactive resolution). */
1391
+ export type ApiPathParamsInput<K extends AllOps> = _ApiPathParamsInput<operations, K>
1392
+ /** Query parameters type. */
1393
+ export type ApiQueryParams<K extends AllOps> = _ApiQueryParams<operations, K>`;
1394
+ // Re-exports
1395
+ // Use type-only wildcard export to avoid duplicate identifier errors
1396
+ const reExports = schemaEnumNames.length > 0
1397
+ ? schemaEnumNames.map((n) => `export { ${n} } from './api-enums'`).join('\n') +
1398
+ "\nexport type * from './api-enums'"
1399
+ : '// No schema-level enums to re-export';
1400
+ return `// Auto-generated from OpenAPI specification - do not edit manually
1401
+
1402
+ import type { operations } from './openapi-types.js'
1403
+ import { HttpMethod } from '@qualisero/openapi-endpoint'
1404
+ import type {
1405
+ ApiResponse as _ApiResponse,
1406
+ ApiResponseSafe as _ApiResponseSafe,
1407
+ ApiRequest as _ApiRequest,
1408
+ ApiPathParams as _ApiPathParams,
1409
+ ApiPathParamsInput as _ApiPathParamsInput,
1410
+ ApiQueryParams as _ApiQueryParams,
1411
+ } from '@qualisero/openapi-endpoint'
1412
+
1413
+ export type { operations }
1414
+
1415
+ ${reExports}
1416
+
1417
+ // ============================================================================
1418
+ // Per-operation enum values
1419
+ // ============================================================================
1420
+
1421
+ ${enumConsts}
1422
+
1423
+ // ============================================================================
1424
+ // Operations map (kept for inspection / backward compatibility)
1425
+ // ============================================================================
1426
+
1427
+ const operationsBase = {
1428
+ ${opEntries}
1429
+ } as const
1430
+
1431
+ export const openApiOperations = operationsBase as typeof operationsBase & Pick<operations, keyof typeof operationsBase>
1432
+ export type OpenApiOperations = typeof openApiOperations
1433
+
1434
+ // ============================================================================
1435
+ // Convenience type aliases
1436
+ // ============================================================================
1437
+ ${typeHelpers}
1438
+ `;
1439
+ }
1440
+ /**
1441
+ * Async wrapper for generateApiOperationsContent.
1442
+ */
1443
+ async function generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames) {
1444
+ console.log('🔨 Generating api-operations.ts...');
1445
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1446
+ const opEnums = buildOperationEnums(openApiSpec, operationMap);
1447
+ const content = generateApiOperationsContent(operationMap, opEnums, schemaEnumNames);
1448
+ fs.writeFileSync(path.join(outputDir, 'api-operations.ts'), content);
1449
+ console.log(`✅ Generated api-operations.ts (${Object.keys(operationMap).length} operations)`);
1450
+ }
1451
+ // ============================================================================
1452
+ // New generators: api-types.ts
1453
+ // ============================================================================
1454
+ /**
1455
+ * Generates the content for `api-types.ts` file.
1456
+ * @param operationMap The operation info map
1457
+ * @param opEnums Per-operation enum fields
1458
+ * @returns Generated TypeScript file content
1459
+ */
1460
+ function generateApiTypesContent(operationMap, opEnums) {
1461
+ const ids = Object.keys(operationMap).sort();
1462
+ const isQuery = (id) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method);
1463
+ const namespaces = ids
1464
+ .map((id) => {
1465
+ const query = isQuery(id);
1466
+ const fields = opEnums[id] ?? {};
1467
+ const enumTypes = Object.entries(fields)
1468
+ .map(([fieldName, vals]) => {
1469
+ const typeName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
1470
+ const union = Object.values(vals)
1471
+ .map((v) => `'${v}'`)
1472
+ .join(' | ');
1473
+ return ` /** \`${union}\` */\n export type ${typeName} = ${union}`;
1474
+ })
1475
+ .join('\n');
1476
+ const commonLines = [
1477
+ ` /** Full response type - all fields required. */`,
1478
+ ` export type Response = _ApiResponse<OpenApiOperations, '${id}'>`,
1479
+ ` /** Response type - only \`readonly\` fields required. */`,
1480
+ ` export type SafeResponse = _ApiResponseSafe<OpenApiOperations, '${id}'>`,
1481
+ ];
1482
+ if (!query) {
1483
+ commonLines.push(` /** Request body type. */`, ` export type Request = _ApiRequest<OpenApiOperations, '${id}'>`);
1484
+ }
1485
+ commonLines.push(` /** Path parameters. */`, ` export type PathParams = _ApiPathParams<OpenApiOperations, '${id}'>`, ` /** Query parameters. */`, ` export type QueryParams = _ApiQueryParams<OpenApiOperations, '${id}'>`);
1486
+ const enumNs = enumTypes ? ` export namespace Enums {\n${enumTypes}\n }` : ` export namespace Enums {}`;
1487
+ return ` export namespace ${id} {\n${commonLines.join('\n')}\n${enumNs}\n }`;
1488
+ })
1489
+ .join('\n\n');
1490
+ return `/* eslint-disable */
1491
+ // Auto-generated from OpenAPI specification — do not edit manually
1492
+
1493
+ import type {
1494
+ ApiResponse as _ApiResponse,
1495
+ ApiResponseSafe as _ApiResponseSafe,
1496
+ ApiRequest as _ApiRequest,
1497
+ ApiPathParams as _ApiPathParams,
1498
+ ApiQueryParams as _ApiQueryParams,
1499
+ } from '@qualisero/openapi-endpoint'
1500
+ import type { operations as OpenApiOperations } from './openapi-types.js'
1501
+
1502
+ /**
1503
+ * Type-only namespace for all API operations.
1504
+ *
1505
+ * @example
1506
+ * \`\`\`ts
1507
+ * import type { Types } from './generated/api-types'
1508
+ *
1509
+ * type Pet = Types.getPet.Response
1510
+ * type NewPet = Types.createPet.Request
1511
+ * type PetStatus = Types.createPet.Enums.Status // 'available' | 'pending' | 'adopted'
1512
+ * type Params = Types.getPet.PathParams // { petId: string }
1513
+ * \`\`\`
1514
+ */
1515
+ export namespace Types {
1516
+ ${namespaces}
1517
+ }
1518
+ `;
1519
+ }
1520
+ /**
1521
+ * Async wrapper for generateApiTypesContent.
1522
+ */
1523
+ async function generateApiTypesFile(openApiSpec, outputDir, excludePrefix) {
1524
+ console.log('🔨 Generating api-types.ts...');
1525
+ const operationMap = buildOperationMap(openApiSpec, excludePrefix);
1526
+ const opEnums = buildOperationEnums(openApiSpec, operationMap);
1527
+ const content = generateApiTypesContent(operationMap, opEnums);
1528
+ fs.writeFileSync(path.join(outputDir, 'api-types.ts'), content);
1529
+ console.log(`✅ Generated api-types.ts`);
1530
+ }
990
1531
  async function main() {
991
1532
  const args = process.argv.slice(2);
992
1533
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
@@ -1030,18 +1571,22 @@ async function main() {
1030
1571
  else {
1031
1572
  console.log(`✅ Including all operations (no exclusion filter)`);
1032
1573
  }
1033
- // Fetch OpenAPI spec content
1574
+ // Fetch and parse OpenAPI spec once
1034
1575
  let openapiContent = await fetchOpenAPISpec(openapiInput);
1035
- // Parse spec and add missing operationIds
1036
1576
  const openApiSpec = JSON.parse(openapiContent);
1577
+ // Add missing operationIds
1037
1578
  addMissingOperationIds(openApiSpec);
1038
1579
  openapiContent = JSON.stringify(openApiSpec, null, 2);
1580
+ // Collect schema enum names for re-export
1581
+ const schemaEnumNames = extractEnumsFromSpec(openApiSpec).map((e) => e.name);
1039
1582
  // Generate all files
1040
1583
  await Promise.all([
1041
1584
  generateTypes(openapiContent, outputDir),
1042
- generateApiOperations(openapiContent, outputDir, excludePrefix),
1043
1585
  generateApiEnums(openapiContent, outputDir, excludePrefix),
1044
1586
  generateApiSchemas(openapiContent, outputDir, excludePrefix),
1587
+ generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames),
1588
+ generateApiTypesFile(openApiSpec, outputDir, excludePrefix),
1589
+ generateApiClientFile(openApiSpec, outputDir, excludePrefix),
1045
1590
  ]);
1046
1591
  console.log('🎉 Code generation completed successfully!');
1047
1592
  }